húúúúúúúdenagycommit

This commit is contained in:
Adam 2025-08-26 21:04:55 +02:00
parent de70b8c97f
commit 00417c4cf8
501 changed files with 7265 additions and 1340 deletions

View File

@ -14,6 +14,10 @@
<None Remove="SeemGen.Tests\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="Components\Partials\OverlayEditor.razor" />
</ItemGroup>
<ItemGroup>
<None Remove="Components\Pages\Home.razorOLD" />
</ItemGroup>

View File

@ -6,6 +6,13 @@
<html lang="en">
<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 name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
@ -13,7 +20,8 @@
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="main.css" />
<link rel="stylesheet" href="admin.css" />
<link rel="stylesheet" href="animate.css" />
@* <link rel="stylesheet" href="animate.css" /> *@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
<link rel="stylesheet" href="loader.css" />
<link rel="stylesheet" href="BLAIzor.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
@ -23,21 +31,12 @@
<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 type="text/javascript" src="scripts/background.js"> </script>
<script type="text/javascript" src="scripts/SeemGenCss.js"> </script>
@* <script>
window.applyDynamicCss = (cssContent) => {
let styleTag = document.getElementById('seemgen-style');
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.id = 'seemgen-style';
document.head.appendChild(styleTag);
}
styleTag.textContent = cssContent;
};
</script> *@
@* <RadzenTheme Theme="material-dark" @rendermode="InteractiveServer" /> *@
<RadzenTheme Theme="material" @rendermode="InteractiveServer" />
<script type="text/javascript" src="scripts/whisperRecorder.js"></script>
<RadzenTheme Theme="material-dark" @rendermode="InteractiveServer" />
@* <RadzenTheme Theme="material" @rendermode="InteractiveServer" /> *@
<HeadOutlet />
<SectionOutlet SectionName="HeadContentFromPage" />
</head>
@ -53,5 +52,9 @@
crossorigin="anonymous"></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>
</html>

View File

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

View File

@ -27,43 +27,51 @@ else
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="form-group col-md-4">
<label>Template Name</label>
<InputText class="form-control" @bind-Value="currentTemplate.TemplateName" />
<div class="col-12 col-md-6">
<div class="form-group">
<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 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 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>
</EditForm>

View File

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

View File

@ -11,7 +11,6 @@
@using System.Text.Json
@using Sidio.Sitemap.Blazor
@rendermode InteractiveServer
@inject ContentService _contentService
@inject NavigationManager _navigationManager
@inject IHttpContextAccessor HttpContextAccessor
@inject DesignTemplateService DesignTemplateService
@ -21,15 +20,17 @@
<ErrorBoundary>
<ChildContent>
<div class="page" style="z-index: 1">
<NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" OnMenuClicked=@MenuClick></NewNavMenu>
<NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" SiteId="SiteId" OnMenuClicked=@MenuClick></NewNavMenu>
<main>
<article class="content text-center" style="position: relative; z-index: 4;">
<PageTitle>Home</PageTitle>
@{
if(SiteInfo!= null)
{
if (SiteInfo != null)
{
if (!string.IsNullOrEmpty(SiteInfo.BackgroundVideo))
{
<VideoComponent site="SiteInfo" />
@ -49,7 +50,14 @@
<div class="displaysearch">
<div class="searchBox">
@if (VoiceEnabled)
{
if (STTEnabled)
{
<button id="recButton" class="voicebutton bg-panel-gradient" onclick="startRecording()"><i class="fa-solid fa-microphone"></i></button>
<button id="stopButton" class="voicebutton bg-danger" onclick="stopRecording()" hidden><i class="fa-solid fa-microphone-slash"></i></button>
}
}
<input @oninput="(e) => UserInput = e.Value.ToString()"
@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="#">
@ -60,24 +68,24 @@
@{
@if (VoiceEnabled)
{
if (STTEnabled)
{
<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 (STTEnabled)
// {
// <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)
{
if (!AiVoicePermitted)
{
<button data-hint="listen" class="btn btn-primary voicebutton" @onclick="AllowAIVoice">
<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" @onclick="MuteAI"><i class="fa-solid fa-volume-high"></i></button>
<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>
}
@ -92,50 +100,53 @@
@* </div> *@
</div>
<p id="recordingText"></p>
<div id="currentContent">
@{
if (!string.IsNullOrEmpty(HtmlContent.ToString()))
{
if (isEmailFormVisible)
<div id="currentContent">
<p id="recordingText"></p>
<AnimateOnRender CssClass="animate__animated animate__backInUp">
@{
if (!string.IsNullOrEmpty(HtmlContent.ToString()))
{
<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>
if (isEmailFormVisible)
{
<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>
</div>
}
else
{
<div class="pt-5 @FirstColumnClass">
@((MarkupString)HtmlContent.ToString())
</div>
}
}
else
{
<div class="pt-5 @FirstColumnClass">
@((MarkupString)HtmlContent.ToString())
<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>
}
}
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>
@ -145,6 +156,7 @@
</main>
<FooterComponent MenuString="@Menu" OnMenuClicked=@MenuClick></FooterComponent>
</div>
</ChildContent>
<ErrorContent Context="ex">
<p role="alert">An error occurred: @ex.Message</p>
@ -155,6 +167,7 @@
}
</ErrorContent>
</ErrorBoundary>
<script>
var sessionId = null;
@ -212,6 +225,7 @@
{
if (firstRender)
{
await _logger.InfoAsync("Index component OnafterRender.", $"{SiteId}");
await jsRuntime.InvokeVoidAsync("setSessionId", SessionId);
await jsRuntime.InvokeVoidAsync("initHints");
@ -277,12 +291,14 @@
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);
// 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();
@ -310,7 +326,7 @@
public void HomeClick()
{
//ChatGptService.OnContentReceived -= UpdateContent;
AIService.OnContentReceived -= UpdateContent;
ChatGptService.OnContentReceived -= UpdateContent;
_navigationManager.Refresh(true);
}
@ -328,16 +344,16 @@
protected override async Task OnInitializedAsync()
{
await _logger.InfoAsync("Index component initialized.", $"{SiteId}");
_scopedContentService.OnBrandNameChanged += HandleBrandNameChanged;
// ChatGptService.OnContentReceived += UpdateContent;
AIService.OnContentReceived += UpdateContent;
AIService.OnContentReceiveFinished += UpdateFinished;
ChatGptService.OnContentReceived += UpdateContent;
ChatGptService.OnContentReceiveFinished += UpdateFinished;
// ChatGptService.OnStatusChangeReceived += UpdateStatus;
AIService.OnStatusChangeReceived += UpdateStatus;
AIService.OnTextContentAvailable += UpdateTextContentForVoice;
SessionId = Guid.NewGuid().ToString();
ChatGptService.OnStatusChangeReceived += UpdateStatus;
ChatGptService.OnTextContentAvailable += UpdateTextContentForVoice;
SessionId = _scopedContentService.SessionId;
_instances[SessionId] = this;
VoiceEnabled = configuration?.GetSection("AiSettings")?.GetValue<bool>("VoiceActivated") ?? false;
}
@ -357,17 +373,17 @@
}
}
private async void UpdateTextContentForVoice(string receivedSessionId, string content)
{
Console.WriteLine("UPDATETEXTCONTENT called");
if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{
// private 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");
}
}
// TextContent = content;
// await ConvertTextToSpeech(content);
// //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
// }
// }
private async void UpdateFinished(string receivedSessionId)
{
@ -418,15 +434,20 @@
dynamicallyLoadedCss = "";
HtmlContent.Clear();
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
AIService.OnContentReceived -= UpdateContent;
AIService.OnContentReceiveFinished -= UpdateFinished;
AIService.OnStatusChangeReceived -= UpdateStatus;
AIService.OnTextContentAvailable -= UpdateTextContentForVoice;
ChatGptService.OnContentReceived -= UpdateContent;
ChatGptService.OnContentReceiveFinished -= UpdateFinished;
ChatGptService.OnStatusChangeReceived -= UpdateStatus;
ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice;
}
public async ValueTask DisposeAsync()
{
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

@ -2,6 +2,7 @@
using BLAIzor.Services;
using Google.Cloud.Speech.V1;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc;
using Microsoft.JSInterop;
using Radzen;
using System.Text;
@ -18,8 +19,10 @@ namespace BLAIzor.Components.Pages
[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] NotificationService NotificationService { get; set; }
[Inject] protected NotificationService NotificationService { get; set; }
[Inject] protected CacheService CacheService { get; set; }
public static readonly Dictionary<string, MainPageBase> _instances = new();
@ -40,7 +43,7 @@ namespace BLAIzor.Components.Pages
public bool STTEnabled;
public bool _initVoicePending = false;
public bool welcomeStage = true;
public bool AiVoicePermitted = false;
public bool AiVoicePermitted = true;
public string FirstColumnClass = "";
public bool isEmailFormVisible = false;
@ -53,6 +56,8 @@ namespace BLAIzor.Components.Pages
public List<MenuItem> MenuItems = new();
public string dynamicallyLoadedCss = string.Empty;
public MenuItem currentMenuItem = new();
public bool IsContentSaved = false;
public WebsiteContentModel SiteModel = new();
@ -233,6 +238,16 @@ namespace BLAIzor.Components.Pages
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
{
@ -243,6 +258,18 @@ namespace BLAIzor.Components.Pages
}
}
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";
@ -348,7 +375,15 @@ namespace BLAIzor.Components.Pages
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

@ -33,9 +33,6 @@ else
<RadzenRow class="rz-text-align-center" Gap="1rem">
@foreach (var image in files.Images)
{

View File

@ -11,8 +11,7 @@
@using System.Net
@using Radzen.Blazor.Rendering
@rendermode InteractiveServer
@inject ContentService _contentService
@* @rendermode InteractiveServer *@
@inject IEmailSender _emailService
@inject NavigationManager _navigationManager
@inject IHttpContextAccessor HttpContextAccessor
@ -21,31 +20,166 @@
@inject CssInjectorService CssService
@inject DialogService DialogService
<div class="page" style="z-index: 1">
<RadzenButton Click="@(args => ToggleSettings())" style="position: fixed;
<div class="top-panel-outer">
<RadzenStack class="top-panel px-2 animate__animated animate__slideInDown" JustifyContent="JustifyContent.Center" Orientation="Orientation.Horizontal" style="margin: auto; align-content:center;" Visible=@displayTopPanel>
@{if(displayOptions) {
<div class="rz-p-2 rz-text-align-center align-content-center">
<strong>@currentMenuItem.Name</strong>
</div>
<div class="rz-p-2 rz-text-align-center align-content-center">
<span>Happy with this look?</span>
<RadzenButton Size="ButtonSize.ExtraSmall" class="btn m-0" Text="@(IsContentSaved ? "Overwrite" : "Save")" Click="@(args => SaveCurrentLayout(currentMenuItem))" />
</div>
<div class="rz-p-2 rz-text-align-center align-content-center">
<span>Not happy?</span>
<RadzenButton Size="ButtonSize.ExtraSmall" class="btn m-0" @ref=button Text="Regenerate" Click="@(args => MenuClick(currentMenuItem.Name))" />
</div>
<div class="rz-p-2 rz-text-align-center align-content-center">
<span>Typos?</span>
<RadzenButton Size="ButtonSize.ExtraSmall" class="btn m-0" @ref=button Text="Manual edit" Click="@(args => OpenContentEditor(currentMenuItem))" />
</div>
<div class="rz-p-2 rz-text-align-center align-content-center">
<span>Cancel</span>
<RadzenButton Size="ButtonSize.ExtraSmall" class="btn m-0" @ref=button Text="x" Click="@(args => ToggleSavedContentEdit())" />
</div>
}
else
{
<p class="text-center">If you navigate to a menu item, you can save it's actual design for consitent looks, and faster load for the visitors</p>
}
}
</RadzenStack>
</div>
<RadzenButton Click="@(args => ToggleSavedContentEdit())" Visible="@(currentMenuItem == null ? false : true)" style="position: fixed;
z-index: 10005;
top: 100px;
left: 0px;
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
left: 10px;
height: 40px;
border-bottom-left-radius: 20px;
border-top-left-radius: 20px;
border-bottom-right-radius: 20px;
background: linear-gradient(307deg, rgba(12, 37, 51, 0.83) 7%, rgb(152 87 199 / 73%) 96%);
border-top-right-radius: 20px;">
<i class="fa-solid fa-pencil"></i>
</RadzenButton>
<RadzenButton Click="@(args => ToggleSettings())" style="position: fixed;
z-index: 10005;
top: 150px;
left: 10px;
height: 40px;
border-bottom-left-radius: 20px;
border-top-left-radius: 20px;
border-bottom-right-radius: 20px;
background: linear-gradient(307deg, rgba(12, 37, 51, 0.83) 7%, rgb(152 87 199 / 73%) 96%);
border-top-right-radius: 20px;">
<i class="fa-solid fa-wrench"></i>
</RadzenButton>
<RadzenStack class="editor-window animate__animated animate__slideInLeft" Orientation="Orientation.Vertical" style="" Visible=@displaySettingsPanel>
<RadzenStack Orientation="Orientation.Vertical" style="">
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="EditSite">
<div class="px-3 py-2 reference-button bg-panel-gradient pointer">
<div class="text-content">
<strong>Edit site</strong>
<br />
<small class="text-muted">Edit basic settings</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-regular fa-circle-question"></i></div>
</div>
</div>
</button>
@*<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="EditSite">
<div class="px-3 py-2 reference-button bg-panel-gradient pointer">
<div class="text-content">
<strong>AI settings</strong>
<br />
<small class="text-muted">Edit AI settings</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-hexagon-nodes-bolt"></i></div>
</div>
</div>
</button>*@
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="OpenManageContentGroups">
<div class="px-3 py-2 reference-button bg-panel-gradient pointer">
<div class="text-content">
<strong>Edit content</strong>
<br />
<small class="text-muted">Manage content</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-file-lines"></i></div>
</div>
</div>
</button>
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="EditMenu">
<div class="px-3 py-2 reference-button bg-panel-gradient pointer">
<div class="text-content">
<strong>Setup menu</strong>
<br />
<small class="text-muted">Link content to the menu</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-bars"></i></div>
</div>
</div>
</button>
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="EditMenu">
<div class="px-3 py-2 reference-button bg-panel-gradient pointer">
<div class="text-content">
<strong>Edit looks</strong>
<br />
<small class="text-muted">Select design</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-eye"></i></div>
</div>
</div>
</button>
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="OpenManageUploads">
<div class="px-3 py-2 reference-button bg-panel-gradient pointer">
<div class="text-content">
<strong>Media</strong>
<br />
<small class="text-muted">Manage your media</small>
</div>
<div class="icon-buttons">
<div class="icon-circle"><i class="fa-solid fa-photo-film"></i></div>
</div>
</div>
</button>
@* <RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h3>Basic information</h3>
<p>@SiteInfo.SiteName</p>
<p>@SiteInfo.SiteName</p>
<RadzenButton class="btn" Text="Edit basic info" Click="@EditSite" />
</div>
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Manage content</h5>
<h3>Site menu</h3>
<RadzenButton class="btn" Text="Edit menu" Click="@EditMenu" />
</div>
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Manage content</h5>
<RadzenButton class="btn" Text="Manage content" Click="@OpenManageContentGroups" />
</div>
@ -75,15 +209,15 @@
</RadzenStack>
<RadzenStack Orientation="Orientation.Vertical" style="">
<div class="rz-p-2 rz-text-align-center">
<h5>Media</h5>
<h5>Media</h5>
<RadzenButton class="btn" Text="Open library" Click="@OpenManageUploads" />
</div>
</RadzenStack>
</RadzenStack>
</RadzenStack>*@
</RadzenStack>
<NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" OnMenuClicked=@MenuClick></NewNavMenu>
<NewNavMenu Menu="@MenuItems" BrandName="@SelectedBrandName" SiteId="siteid" OnMenuClicked=@MenuClick></NewNavMenu>
<main>
<article class="content text-center" style="position: relative; z-index: 4;">
@ -260,8 +394,9 @@
private bool isRecording = false;
// private string dynamicallyLoadedCss = string.Empty;
private bool isContentSaved = false;
private bool displaySettingsPanel = false;
private bool displayTopPanel = false;
private bool displayOptions = false;
private bool forceRegenerate = false;
@ -276,14 +411,22 @@
displaySettingsPanel = !displaySettingsPanel;
}
private void ToggleSavedContentEdit()
{
displayTopPanel = !displayTopPanel;
}
async Task SaveCurrentLayout(MenuItem menuItem)
{
//Save current layout called
menuItem.StoredHtml = HtmlContent.ToString();
Console.WriteLine($"Content length: {HtmlContent.Length}");
Console.WriteLine(menuItem.StoredHtml);
await _logger.InfoAsync($"Preview Component: Saving layout!", $"Content length: {HtmlContent.Length}");
await _logger.InfoAsync($"Preview Component: Saving layout!", $"Content length: {menuItem.StoredHtml}");
var result = await _contentEditorService.UpdateMenuItemAsync(menuItem);
Console.WriteLine($"menuitem updated: {result.Id}, {result.Name}");
var message = NotificationHelper.CreateNotificationMessage("Page design saved", 2, "Success", $"Page design for {result.Name} updated successfully");
ShowNotification(message);
await _logger.InfoAsync($"Preview Component: Saved layout for page!", $"MenuItem updated with new design: {result.Id}, {result.Name}");
}
@ -327,7 +470,7 @@
public void HomeClick()
{
//ChatGptService.OnContentReceived -= UpdateContent;
AIService.OnContentReceived -= UpdateContent;
ChatGptService.OnContentReceived -= UpdateContent;
_navigationManager.Refresh(true);
}
@ -346,7 +489,7 @@
protected override async Task OnParametersSetAsync()
{
SessionId = Guid.NewGuid().ToString();
SessionId = _scopedContentService.SessionId;
SiteId = siteid;
_instances[SessionId] = this;
_scopedContentService.OnBrandNameChanged += HandleBrandNameChanged;
@ -392,10 +535,10 @@
Console.Write("------------------------");
// ChatGptService.OnContentReceived += UpdateContent;
AIService.OnContentReceived += UpdateContent;
ChatGptService.OnContentReceived += UpdateContent;
// ChatGptService.OnStatusChangeReceived += UpdateStatus;
AIService.OnStatusChangeReceived += UpdateStatus;
AIService.OnTextContentAvailable += UpdateTextContentForVoice;
ChatGptService.OnStatusChangeReceived += UpdateStatus;
ChatGptService.OnTextContentAvailable += UpdateTextContentForVoice;
Menu = await GetMenuList(SiteId);
MenuItems = await GetMenuItems(SiteId);
@ -409,7 +552,7 @@
else
{
await ChatGptService.GetChatGptWelcomeMessage(SessionId, SiteId, TemplateCollectionName, Menu);
SiteModel = await ChatGptService.InitSite(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);
}
@ -428,7 +571,10 @@
if (menuItem != null)
{
currentMenuItem = menuItem;
// IsContentSaved = string.IsNullOrEmpty(currentMenuItem.StoredHtml) ? false : true;
// _logger.InfoAsync($"Preview - UpdateContent: {IsContentSaved}");
displayOptions = true;
StateHasChanged();
}
//InvokeAsync(StateHasChanged); // Ensures UI updates dynamically
await InvokeAsync(() =>
@ -441,16 +587,16 @@
}
}
private async void UpdateTextContentForVoice(string receivedSessionId, string content)
{
if (receivedSessionId == SessionId) // Only accept messages meant for this tab
{
// private async void UpdateTextContentForVoice(string receivedSessionId, string content)
// {
// if (receivedSessionId == SessionId) // Only accept messages meant for this tab
// {
TextContent = content;
await ConvertTextToSpeech(content);
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
}
}
// TextContent = content;
// await ConvertTextToSpeech(content);
// //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
// }
// }
private async void UpdateFinished(string receivedSessionId)
{
@ -494,19 +640,30 @@
}
}
public void Dispose()
// public void Dispose()
// {
// dynamicallyLoadedCss = "";
// HtmlContent.Clear();
// _scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
// AIService.OnContentReceived -= UpdateContent;
// AIService.OnStatusChangeReceived -= UpdateStatus;
public async ValueTask DisposeAsync()
{
dynamicallyLoadedCss = "";
HtmlContent.Clear();
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
AIService.OnContentReceived -= UpdateContent;
AIService.OnStatusChangeReceived -= UpdateStatus;
}
public async ValueTask DisposeAsync()
{
await CssTemplateService.DeleteSessionCssFile(SessionId);
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
ChatGptService.OnContentReceived -= UpdateContent;
ChatGptService.OnContentReceiveFinished -= UpdateFinished;
ChatGptService.OnStatusChangeReceived -= UpdateStatus;
ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice;
}
// public async ValueTask DisposeAsync()
// {
// await CssTemplateService.DeleteSessionCssFile(SessionId);
// }
public async Task EditSite()
{
@ -523,21 +680,59 @@
await DialogService.OpenAsync<ManageSiteInfoPartial>($"Edit information of {SiteInfo.SiteName}",
new Dictionary<string, object>() {
{ "SiteId", siteid },
{ "SiteId", siteid },
{ "OnSiteNameChanged", new Func<string, Task>(OnSiteNameChanged) },
},
{ "OnSiteInfoSaveClicked", new Func<SiteInfo, Task>(OnSiteInfoSaved) },
},
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
public async Task EditMenu()
{
string dialogKey = $"EditMenuDialogSettings_{siteid}"; // can be anything unique
var settings = await LoadStateAsync(dialogKey)
?? new EditSiteInfoDialogSettings
{
Width = "500px",
Height = "512px",
Left = "10%",
Top = "10%"
};
await DialogService.OpenAsync<MenuItemEditor>($"Edit menu of {SiteInfo.SiteName}",
new Dictionary<string, object>() {
{ "SiteId", siteid },
{ "SessionId", SessionId },
// { "OnSiteNameChanged", new Func<string, Task>(OnSiteNameChanged) },
// { "OnSiteInfoSaveClicked", new Func<SiteInfo, Task>(OnSiteInfoSaved) },
},
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
@ -549,6 +744,15 @@
SelectedBrandName = newName;
}
private async Task OnSiteInfoSaved(SiteInfo newInfo)
{
Console.WriteLine("Sitename updated!!!!!!");
var message = NotificationHelper.CreateNotificationMessage("Site information saved", 2, "Success", "Site information saved successfully");
ShowNotification(message);
}
public async Task EditContentItem(int Id)
{
string dialogKey = $"EditContentItemDialogSettings_{Id}"; // can be anything unique
@ -568,7 +772,7 @@
{ "OnContentUpdated", new Func<ContentItem, Task>(OnContentItemUpdated) },
{ "OnSaved", new Func<ContentItem, Task>(OnContentItemSaved) },
{ "OnCancelled", new Func<Task>(OnEditContentItemCancelClicked) },
},
},
new DialogOptions()
{
Resizable = true,
@ -588,13 +792,16 @@
private async Task OnContentItemUpdated(ContentItem contentGroup)
{
Console.WriteLine("ContentGroup updated!!!!!!");
}
private async Task OnContentItemSaved(ContentItem contentGroup)
{
Console.WriteLine("ContentGroup edit started!!!!!!");
//TODO Cahce outdated
await CacheService.UpdateContentCache(SessionId, SiteId);
var message = NotificationHelper.CreateNotificationMessage("ContentItem saved", 2, "Success", "ContentItem saved successfully");
ShowNotification(message);
}
@ -614,7 +821,7 @@
if (settings != null)
{
Console.WriteLine($"Settings: {settings.Top}, {settings.Left}, {settings.Height}, {settings.Width}");
}
}
else
{
settings = new EditSiteInfoDialogSettings
@ -628,9 +835,9 @@
await DialogService.OpenAsync<ManageContentGroupsPartial>($"Manage content of {SiteInfo.SiteName}",
new Dictionary<string, object>() {
{ "SiteInfoId", siteid },
{ "SiteInfoId", siteid },
{ "OnManageContentItemClicked", new Func<string, int, Task>(OnManageContentItemClicked) }
},
},
new DialogOptions()
{
@ -652,28 +859,23 @@
private async Task OnContentGroupUpdated(ContentGroup contentGroup)
{
Console.WriteLine("ContentGroup updated!!!!!!");
}
private async Task OnContentGroupEditStarted(ContentGroup contentGroup)
{
Console.WriteLine("ContentGroup edit started!!!!!!");
}
private async Task OnManageContentItemClicked(string methodName, int Id)
{
Console.WriteLine("ContentItem Edit clicked");
await EditContentItem(Id);
}
public async Task OpenManageUploads()
{
string dialogKey = $"ManageUploadsDialogSettings_{siteid}"; // can be anything unique
var settings = await LoadStateAsync(dialogKey)
?? new EditSiteInfoDialogSettings
{
@ -698,10 +900,56 @@
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
public async Task OpenContentEditor(MenuItem currentMenuItem)
{
string dialogKey = $"HtmlContentEditorSettings_{siteid}"; // can be anything unique
// <HtmlContentEditor Html="@CurrentHtml" HtmlChanged="@OnHtmlUpdated" />
var settings = await LoadStateAsync(dialogKey)
?? new EditSiteInfoDialogSettings
{
Width = "600px",
Height = "600px",
Left = "10%",
Top = "10%"
};
await DialogService.OpenAsync<HtmlContentEditor>($"Edit the content of this page",
new Dictionary<string, object>() {
{ "Html", currentMenuItem.StoredHtml },
{ "HtmlChanged", new Func<string, Task>(OnHtmlUpdated) }
},
new DialogOptions()
{
Resizable = true,
Draggable = true,
Resize = GetResizeHandler(dialogKey),
Drag = GetDragHandler(dialogKey),
Width = settings.Width,
Height = settings.Height,
Left = settings.Left,
Top = settings.Top,
CssClass = "draggable-popup-dialog",
WrapperCssClass = "draggable-popup-dialog-wrapper"
});
await SaveStateAsync(dialogKey, _editSiteinfoSettings);
}
private async Task OnHtmlUpdated(string newHtml)
{
HtmlContent.Clear();
HtmlContent.Append(newHtml);
// You can trigger embedding regeneration, saving, etc.
currentMenuItem.StoredHtml = newHtml;
var result = await _contentEditorService.UpdateMenuItemAsync(currentMenuItem);
var message = NotificationHelper.CreateNotificationMessage("Content saved", 2, "Success", "Static content saved successfully");
ShowNotification(message);
}
private Action<System.Drawing.Point> GetDragHandler(string key)
{

View File

@ -14,15 +14,20 @@
<h1>Your Sites</h1>
<div class="row g-0">
<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"))>
<HeaderTemplate>
<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>
</HeaderTemplate>
<ChildContent>
<RadzenCard class="rz-mt-4">
<CreateSiteWizard OnDescriptionFinalized="HandleDescriptionGenerated" UserId="@userId" />
@* <RadzenCard class="rz-mt-4">
<EditForm Model="newSite" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
@ -47,74 +52,95 @@
<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>
<button type="submit" class="btn btn-primary">Create Site</button>
</EditForm>
</RadzenCard>
</RadzenCard>*@
</ChildContent>
</ChildContent>
</RadzenPanel>
@if (siteInfoList.Any())
{
{
<RadzenPanel AllowCollapse="true" class="rz-my-5 rz-mx-auto" Style="width: 100%"
Expand=@(() => Change("Panel expanded")) Collapse=@(() => Change("Panel collapsed"))>
<HeaderTemplate>
<RadzenText TextStyle="TextStyle.H6" class="rz-display-flex rz-align-items-center rz-m-0">
<RadzenIcon Icon="account_box" class="rz-me-1" /><b>Sites created</b>
</RadzenText>
</HeaderTemplate>
<ChildContent>
<RadzenCard class="rz-mt-4">
<RadzenDataList PageSize="3" WrapItems="true" AllowPaging="true"
Data="@siteInfoList" TItem="SiteInfo">
<Template Context="site">
<RadzenCard Style="width: 250px; background-color: darkgrey">
<RadzenRow JustifyContent="@JustifyContent.SpaceBetween">
<RadzenColumn Size="4" class="rz-text-truncate">
<RadzenBadge BadgeStyle="BadgeStyle.Light" Text=@($"{site.Id}") class="rz-me-1" />
<b>@(site.SiteName)</b>
</RadzenColumn>
<RadzenColumn Size="8" class="rz-text-align-end">
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text=@($"new") />
</RadzenColumn>
</RadzenRow>
<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">
<RadzenImage Path="@site.BrandLogoUrl" class="img-fluid" Style="max-height: 100px;" AlternateText="@(site.SiteName)" />
<RadzenStack Gap="0">
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-0">@(site.SiteName)</RadzenText>
<RadzenText TextStyle="TextStyle.Body1">@site.DefaultUrl</RadzenText>
<RadzenText TextStyle="TextStyle.Body2">Fefault color: @(site.DefaultColor)</RadzenText>
<RadzenText TextStyle="TextStyle.Body2">Theme: @(site.TemplateId)</RadzenText>
</RadzenStack>
</RadzenStack>
<RadzenStack Orientation="@Orientation.Horizontal" Gap="10px" Reverse="false" JustifyContent="@JustifyContent.Center" AlignItems="@AlignItems.Center" Wrap="@FlexWrap.Wrap" Style="height: fit-content">
<InputText @bind-Value="collectionName" class="form-control" style="width: 100%;" placeholder="Site name" />
<a style="font-size: 14px;" @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>
<RadzenPanel AllowCollapse="true" class="rz-my-5 rz-mx-auto" Style="width: 100%"
Expand=@(() => Change("Panel expanded")) Collapse=@(() => Change("Panel collapsed"))>
<HeaderTemplate>
<RadzenText TextStyle="TextStyle.H6" class="rz-display-flex rz-align-items-center rz-m-0">
<RadzenIcon Icon="account_box" class="rz-me-1" /><b>Sites created</b>
</RadzenText>
</HeaderTemplate>
<ChildContent>
@{
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">
<Template Context="site">
<RadzenCard class="admin-rz-card" Style="width: 250px;">
<RadzenRow JustifyContent="@JustifyContent.SpaceBetween">
<RadzenColumn Size="4" class="rz-text-truncate">
<RadzenBadge BadgeStyle="BadgeStyle.Light" Text=@($"{site.Id}") class="rz-me-1" />
<b>@(site.SiteName)</b>
</RadzenColumn>
<RadzenColumn Size="8" class="rz-text-align-end">
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text=@($"new") />
</RadzenColumn>
</RadzenRow>
<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">
<RadzenImage Path="@site.BrandLogoUrl" class="img-fluid" Style="max-height: 100px;" AlternateText="@(site.SiteName)" />
<RadzenStack Gap="0">
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-0">@(site.SiteName)</RadzenText>
<RadzenText TextStyle="TextStyle.Body1">@site.DefaultUrl</RadzenText>
<RadzenText TextStyle="TextStyle.Body2">Fefault color: @(site.DefaultColor)</RadzenText>
<RadzenText TextStyle="TextStyle.Body2">Theme: @(site.TemplateId)</RadzenText>
</RadzenStack>
</RadzenStack>
<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>
</RadzenStack>
</RadzenStack>
</RadzenCard>
</Template>
</RadzenDataList>
</RadzenCard>
</AnimateOnRender>
}
else
{
<RadzenCard class="rz-mt-4">
<p>Let1s create your first website!</p>
</RadzenCard>
</Template>
</RadzenDataList>
</RadzenCard>
</ChildContent>
<SummaryTemplate>
<RadzenCard class="rz-mt-4">
<b>@siteInfoList.Count() Sites</b>
</RadzenCard>
</SummaryTemplate>
</RadzenPanel>
}
}
</ChildContent>
<SummaryTemplate>
<RadzenCard class="rz-mt-4">
<b>@siteInfoList.Count() Sites</b>
</RadzenCard>
</SummaryTemplate>
</RadzenPanel>
@* foreach (var item in siteInfoList)
@* foreach (var item in siteInfoList)
{
<div class="col-xs-12 col-md-4">
<div class="card m-2">
@ -140,11 +166,12 @@
</div>
</div>
} *@
}
else
{
<p>No sites created yet.</p>
}
}
else
{
<p>No sites created yet.</p>
}
</AnimateOnRender>
</div>
@ -157,9 +184,28 @@
private string? userName;
private AuthenticationState? authState;
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)
{
Console.Write($"{text}");
@ -201,7 +247,9 @@
userName = CustomAuthProvider.GetUserName();
}
siteInfoList = await _contentEditorService.GetUserSitesAsync(userId!);
}
SessionId = SiteInfoService.SessionId;
// SiteInfoService.SessionId = SessionId;
}
private async Task HandleValidSubmit()
{
@ -212,6 +260,11 @@
var result = await _contentEditorService.AddSiteInfoAsync(newSite);
siteInfoList = await _contentEditorService.GetUserSitesAsync(userId!);
newSite = new(); // Reset the form
if(result != null)
{
NavigationManager.NavigateTo($"/generate-content/{result.Id}");
}
}
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

@ -28,7 +28,7 @@
<button class="pointer bg-transparent border-0" style="width:100%; text-align: left; " @onclick="CreateNewItem">
<div class="px-3 py-2 reference-button bg-panel-gradient-highlight pointer">
<div class="text-content">
<strong>Add new</strong>
<strong>Add new content</strong>
<br />
<small class="text-muted">Add a new content to this group</small>
</div>
@ -45,12 +45,12 @@
<li class="list-group-item">
<div class="p-2 reference-button">
<div class="text-content">
<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">
<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> *@

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

@ -36,7 +36,7 @@
@* <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-warning" type="button" @onclick="() => OnForceReChunkClicked(Group)">ReChunk</button> *@
<button class="btn btn-danger" type="button" @onclick="() => DeleteGroup(Group)">Delete</button>
</div>
</div>

View File

@ -1,6 +1,7 @@
@using BLAIzor.Models
@using BLAIzor.Services
@inject ContentEditorService contentEditorService
@inject CacheService cache
@if (isLoading)
{
@ -73,7 +74,7 @@ else
contentItem!.LastUpdated = DateTime.UtcNow;
await contentEditorService.SaveAndSyncContentItemAsync(contentItem, contentItem.ContentGroup.SiteInfo.VectorCollectionName, false);
if (OnSaved != null)
await OnSaved.Invoke(contentItem);
}

View File

@ -215,7 +215,10 @@ else if (!string.IsNullOrEmpty(ErrorMessage))
{
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 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
}

View File

@ -167,7 +167,10 @@ else if (!string.IsNullOrEmpty(errorMessage))
{
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();
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)
{

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

@ -36,7 +36,7 @@ else
<button class="pointer bg-transparent border-0 p-0" style="width:100%; text-align: left;" @onclick="AddNewGroup">
<div class="mb-2 p-3 reference-button bg-panel-gradient-highlight pointer">
<div class="text-content">
<strong>Add new</strong>
<strong>Add new group</strong>
<br />
<small class="text-muted">Add a new content group like "Blog", News, "Manuals" etc...</small>
</div>

View File

@ -84,7 +84,7 @@ else
[Parameter]
public int SiteId { get; set; }
[Parameter] public Func<ContentGroup, Task> OnSiteInfoSaveClicked { 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; }
@ -116,6 +116,8 @@ else
private async Task SaveSiteInfo()
{
await _contentEditorService.UpdateSiteInfoAsync(siteInfo);
if (OnSiteInfoSaveClicked != null)
await OnSiteInfoSaveClicked.Invoke(siteInfo);
}
private async void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)

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

@ -5,10 +5,11 @@
@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>
@ -32,7 +33,7 @@
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
data-bs-toggle="dropdown" aria-expanded="false">
@menuItem.Name
</a>
<ul class="dropdown-menu">
@ -69,10 +70,10 @@
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li> *@
</ul>
<select @onchange="OnLanguageSelected" class="form-select" style="width:100px">
<option value="">Lang</option>
<option value="@SelectedLanguage">@SelectedLanguage</option>
@foreach (var Language in Languages)
{
<option value="@Language">@Language</option>
@ -84,7 +85,8 @@
@code {
public int SiteId;
[Parameter]
public int SiteId { get; set; }
[Parameter]
public List<MenuItem> Menu { get; set; }
[Parameter] public string? BrandName { get; set; } = "BLAIzor";
@ -114,17 +116,8 @@
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()
@ -133,6 +126,28 @@
{
//
}
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();
}
@ -147,4 +162,6 @@
}
}
}

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>
<div class="form-group">
<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 />
<RadzenHtmlEditorRedo />
<RadzenHtmlEditorSource />
</RadzenHtmlEditor>
</RadzenHtmlEditor> *@
</div>
</div>
<EventConsole @ref=@console />
@* <EventConsole @ref=@console /> *@
@if (IsLoading)
{
<p>Loading content...</p>
@ -83,7 +83,7 @@
private string? userName;
private AuthenticationState? authState;
EventConsole console;
// EventConsole console;
public async void ShowSnippetPreview(HtmlSnippet snippet)
{
@ -117,30 +117,30 @@
async Task OnPaste(HtmlEditorPasteEventArgs args)
{
console.Log($"Paste: {args.Html}");
// console.Log($"Paste: {args.Html}");
// await OnContentUpdated.InvokeAsync(MenuItem);
}
async Task OnChange(string html)
{
console.Log($"Change: {html}");
// console.Log($"Change: {html}");
}
async Task OnInput(string html)
{
console.Log($"Input: {html}");
// console.Log($"Input: {html}");
}
void OnExecute(HtmlEditorExecuteEventArgs args)
{
console.Log($"Execute: {args.CommandName}");
// console.Log($"Execute: {args.CommandName}");
}
void OnUploadComplete(UploadCompleteEventArgs args)
{
console.Log($"Upload complete: {args.RawResponse}");
// console.Log($"Upload complete: {args.RawResponse}");
}
}

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
public static class TextHelper
{
// Special character replacement map
private static readonly Dictionary<string, string> SpecialCharacterMap = new()
private static readonly Dictionary<string, string> HungarianSpecialCharacterMap = new()
{
{ "/", " per " },
{ "@", " kukac " },
@ -17,27 +17,22 @@ public static class TextHelper
//{ " - ", " mínusz " } // Example, you can add more
};
public static string ReplaceNumbersAndSpecialCharacters(string text)
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 email addresses
//text = Regex.Replace(text, @"\b[\w\.-]+@[\w\.-]+\.\w+\b", match =>
//{
// string key = $"__EMAIL__{protectedParts.Count}__";
// protectedParts[key] = match.Value;
// return key;
//});
//// Protect URLs
//text = Regex.Replace(text, @"https?://[^\s]+", match =>
//{
// string key = $"__URL__{protectedParts.Count}__";
// protectedParts[key] = match.Value;
// return key;
//});
// Protect dates like 2024.05.06
text = Regex.Replace(text, @"\b\d{4}\.\d{2}\.\d{2}\b", match =>
{
@ -55,31 +50,54 @@ public static class TextHelper
var parts = match.Value.Split('.');
var integerPart = int.Parse(parts[0]);
var decimalPart = int.Parse(parts[1]);
return $"{NumberToHungarian(integerPart)} egész {NumberToHungarian(decimalPart)} {(parts[1].Length == 1 ? "tized" : parts[1].Length == 2 ? "század" : "ezred")}";
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);
return NumberToHungarian(number);
if(language == "Hungarian")
{
return NumberToHungarian(number);
}
else
{
return NumberToEnglish(number);
}
});
// Replace special characters from dictionary
foreach (var kvp in SpecialCharacterMap)
if(language == "Hungarian")
{
text = text.Replace(kvp.Key, kvp.Value);
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 ");
// 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);
}
// Restore protected parts
foreach (var kvp in protectedParts)
{
text = text.Replace(kvp.Key, kvp.Value);
}
return text;
}
@ -91,6 +109,7 @@ public static class TextHelper
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();
@ -123,7 +142,7 @@ public static class TextHelper
if (number >= 10)
{
int tensPart = number / 10;
result.Append(tens[tensPart]);
result.Append(tensAlternate[tensPart]);
number %= 10;
}
@ -139,6 +158,84 @@ public static class TextHelper
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("```"))

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,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

@ -392,8 +392,10 @@ namespace BLAIzor.Migrations
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DesignStyle")
.HasColumnType("nvarchar(max)");
b.Property<string>("DomainUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("EmbeddingService")
@ -402,6 +404,12 @@ namespace BLAIzor.Migrations
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");
@ -423,6 +431,9 @@ namespace BLAIzor.Migrations
b.Property<int?>("TemplateId")
.HasColumnType("int");
b.Property<string>("TwitterUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");

View File

@ -218,9 +218,10 @@ namespace BLAIzor.Models
"- 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 displayed as html \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";
@ -232,75 +233,7 @@ namespace BLAIzor.Models
{
public static string GetHtmlRenderingSystemPromptForTextAndErrorResult(string language, string pageTitle, List<HtmlSnippet> htmlToUse, Dictionary<string, string>? photos, string[]? topics)
{
//var sb = new StringBuilder("You are a helpful assistant generating HTML content in " + language + " using Bootstrap.\n\n" +
// "### Rules to Follow: \n" +
// "- Please generate clean and structured HTML that goes between the menu and the footer of a Bootstrap5 webpage.\n" +
// "- DO NOT include `<head>`, `<body>` tags — only content that goes inside the Bootstrap container.\n" +
// "- Use `<h1 class='p-3'>` for the title: " + pageTitle + ".\n" +
// "- Structure content using **separate `row` elements** for different sections.\n" +
// "- Use Bootstrap classes to ensure proper spacing and alignment.\n" +
// " - Use 'row justify-content-center' for layout, 'col-xx-x' classes for columns (ONLY IF multiple columns are needed), and always use 'img-fluid' class for images.\r\n" +
// " - Do NOT use 'col' class if there is only one column to display in the row.\n" +
// "- Do NOT use additional class for paragraphs!\n" +
// "- Do NOT nest images inside paragraphs!\n" +
// "- Ensure clear **separation of content into multiple sections if multiple snippets are provided**.\n");
//if (htmlToUse != null && htmlToUse.Any())
//{
// sb.AppendLine("### Using Provided Snippets:\n" +
// "- You have been given **multiple HTML snippets**:\n");
// foreach (var snippet in htmlToUse)
// {
// sb.AppendLine($"{snippet.Id}: {snippet.Name}: {snippet.Html}.\n");
// sb.AppendLine($"Type: {snippet.Type}, Tags: {snippet.Tags}, Variant: {snippet.Variant}.\n");
// }
// sb.AppendLine("**DO NOT merge them into one**.\n" +
// "- Use each snippet **as a separate section** inside its own div.\n" +
// "- Using all the snippets is NOT mandatory, use them only if you identified content that fits in a specific snippet.\n" +
// "- Validate if there are multiple variants of a snippet, and choose the best option, for example if the block has text and photo url, choose the variant that has image in it.\n" +
// "- Do NOT push all text of a block in the title <h> tags, always pick a short title, followed by the rest of the content as a paragraph.\n" +
// "- If a snippet contains a button, ensure it is placed inside a `div.text-center` for proper alignment.\n");
//}
//if (photos != null && photos.Any())
//{
// sb.AppendLine("### Handling Photos:\n" +
// "- ONLY use the provided photo URLs **as they are**, without modification.\n" +
// "- Do **NOT** generate or assume image URLs.\n" +
// "- Use these photos where appropriate:\n" +
// " " + string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}")) + "\n\n" +
// "- Example usage:\n" +
// " <img src='" + photos.First().Value + "' class='img-fluid' alt='" + photos.First().Key + "' />\n" +
// "DO NOT modifiy the photo urls in any way.");
//}
//if (topics != null && topics.Any())
//{
// sb.AppendLine("### Generating Topic Buttons:\n" +
// "Start this section with a title `Related`\n" +
// "- Create a **separate button** for each topic.\n" +
// $"- Make sure the topics are in {language}, if not, translate the name of them." +
// "- Each button should use `btn btn-primary` and call `callAI('{original_non_translated_topicName}')` on click.\n" +
// "- List of topics:\n" +
// " " + string.Join(", ", topics) + "\n\n" +
// "- Example:\n" +
// " <button class='btn btn-primary' onclick='callAI(\"" + topics.FirstOrDefault() + "\")'>" + topics.FirstOrDefault() + "</button>\n" +
// "Put this section always as last, on the bottom of the page.\n");
//}
//else
//{
// sb.AppendLine("- **No topics provided** → **Do NOT generate topic buttons.**");
//}
//sb.AppendLine("- DO **NOT** merge different content sections.\n" +
// "- DO **NOT** wrap the entire content in a single `div`— use separate section tags.\n" +
// "- DO **NOT** modify the photo urls in any way.\n" +
// "- Do **NOT** generate or assume new photo URLs.\n" +
// "- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html\n" +
// "- Do **NOT** add ANY extra content, like assumed prices, or assumed links.\n" +
// "- If the snippet contains an image, but there is no photo url available, SKIP adding the image tag.\n" +
// "- **Never** add explanations or start with ```html syntax markers.\n");
var sb = new StringBuilder($"You are a helpful assistant generating HTML in {language} using Bootstrap 5.\n\n" +
"### General Instructions:\n" +
"- Output only the **HTML content** between the menu and footer.\n" +
@ -358,13 +291,14 @@ namespace BLAIzor.Models
}
sb.AppendLine("\n### DO NOT:\n" +
"- Merge different content blocks.\n" +
"- Remove javascript or <script> tags"+
"- Wrap the full output in a single `div`.\n" +
"- Skip any provided content.\n" +
"- Add assumed text (e.g. prices, links).\n" +
"- Add `<img>` if no image URL exists.\n" +
"- Include explanation, markdown, or ` ```html` markers.\n");
"- 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");
@ -377,7 +311,7 @@ namespace BLAIzor.Models
"- 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.";
"If the block's rawcontent contains photo url, display that photo within that section, not elsewhere. ";
public static string GetHtmlRenderingAssistantMessageForTextAndErrorResult(LayoutPlan layoutPlan)
{

View File

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

View File

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

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,21 +19,26 @@ namespace BLAIzor.Models
public string UserId { get; set; }
[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.")]
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 bool TTSActive { 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

View File

@ -1,6 +1,8 @@
using BLAIzor.Components;
using BLAIzor.Components.Partials;
using BLAIzor.Data;
using BLAIzor.Helpers;
using BLAIzor.Interfaces;
using BLAIzor.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
@ -12,6 +14,8 @@ using Radzen;
using Sidio.Sitemap.AspNetCore;
using Sidio.Sitemap.Blazor;
using Sidio.Sitemap.Core.Services;
using System;
using System.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
@ -35,6 +39,12 @@ builder.Services.Configure<KestrelServerOptions>(options =>
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.
builder.Services.AddRazorComponents()
@ -42,9 +52,20 @@ builder.Services.AddRazorComponents()
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.AddScoped<ISimpleLogger, SimpleLogger>();
builder.Services.AddScoped<AIService>();
builder.Services.AddSingleton<ContentService>();
//builder.Services.AddSingleton<ContentService>();
builder.Services.AddScoped<ScopedContentService>();
builder.Services.AddScoped<QDrantService>();
builder.Services.AddScoped<OpenAIEmbeddingService>();
@ -58,17 +79,32 @@ builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
builder.Services.AddTransient<IEmailSender, EmailService>();
builder.Services.AddScoped<ContentEditorService>();
builder.Services.AddScoped<ContentEditorAIService>();
builder.Services.AddScoped<CssTemplateService>();
builder.Services.AddScoped<DesignTemplateService>();
builder.Services.AddScoped<OpenAIApiService>();
builder.Services.AddScoped<DeepSeekApiService>();
builder.Services.AddScoped<OpenAiRealtimeService>();
builder.Services.AddScoped<CerebrasAPIService>();
builder.Services.AddScoped<KimiApiService>();
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.AddScoped<ISimpleLogger, SimpleLogger>();
builder.Services.AddServerSideBlazor().AddCircuitOptions(options => options.DetailedErrors = true).AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = 1024000; // e.g. 100 KB

View File

@ -26,13 +26,13 @@ using BLAIzor.Helpers;
using System.Collections;
using Qdrant.Client.Grpc;
using Microsoft.CodeAnalysis.Elfie.Model.Tree;
using Microsoft.AspNetCore.Components.Web;
namespace BLAIzor.Services
{
public class AIService
{
private readonly HttpClient _httpClient;
private readonly ContentService _contentService;
private readonly ContentEditorService _contentEditorService;
private readonly ScopedContentService _scopedContentService;
private readonly OpenAIEmbeddingService _openAIEmbeddingService;
@ -41,17 +41,19 @@ namespace BLAIzor.Services
private readonly OpenAiRealtimeService _openAIRealtimeService;
private readonly DeepSeekApiService _deepSeekApiService;
private readonly CerebrasAPIService _cerebrasAPIService;
private readonly KimiApiService _kimiApiService;
private readonly QDrantService _qDrantService;
private readonly NavigationManager _navigationManager;
private readonly LocalVectorSearchService _localVectorSearchService;
private readonly WebsiteContentLoaderService _websiteContentLoaderService;
private readonly ISimpleLogger _logger;
private readonly CacheService _cacheService;
public static IConfiguration? _configuration;
public AIService(HttpClient httpClient,
ContentService contentService,
ContentEditorService contentEditorService,
ScopedContentService scopedContentService,
QDrantService qDrantService,
@ -61,13 +63,15 @@ namespace BLAIzor.Services
DeepSeekApiService deepSeekApiService,
OpenAiRealtimeService openAIRealtimeService,
CerebrasAPIService cerebrasAPIService,
KimiApiService kimiApiService,
NavigationManager navigationManager,
IConfiguration? configuration,
LocalVectorSearchService localVectorSearchService,
WebsiteContentLoaderService websiteContentLoaderService)
WebsiteContentLoaderService websiteContentLoaderService,
ISimpleLogger logger,
CacheService cacheService)
{
_httpClient = httpClient;
_contentService = contentService;
_contentEditorService = contentEditorService;
_scopedContentService = scopedContentService;
_qDrantService = qDrantService;
@ -77,6 +81,7 @@ namespace BLAIzor.Services
_deepSeekApiService = deepSeekApiService;
_openAIRealtimeService = openAIRealtimeService;
_cerebrasAPIService = cerebrasAPIService;
_kimiApiService = kimiApiService;
_navigationManager = navigationManager;
_localVectorSearchService = localVectorSearchService;
_websiteContentLoaderService = websiteContentLoaderService;
@ -85,15 +90,17 @@ namespace BLAIzor.Services
_openAIApiService.RegisterCallback(HandleActionInvoked, HandleFinishedInvoked, HandleErrorInvoked);
_cerebrasAPIService.RegisterCallback(HandleActionInvoked, HandleFinishedInvoked, HandleErrorInvoked);
_openAIRealtimeService.RegisterCallback(HandleActionInvoked);
_logger = logger;
_cacheService = cacheService;
}
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
public string _apiKey;
public static event Action<string, string, MenuItem?>? OnContentReceived;
public static event Action<string>? OnContentReceiveFinished;
public static event Action<string, string>? OnContentReceivedError;
public static event Action<string, string>? OnStatusChangeReceived;
public static event Action<string, string>? OnTextContentAvailable;
public event Action<string, string, MenuItem?>? OnContentReceived;
public event Action<string>? OnContentReceiveFinished;
public event Action<string, string>? OnContentReceivedError;
public event Action<string, string>? OnStatusChangeReceived;
public event Action<string, string>? OnTextContentAvailable;
public string Mood = "cool, and professional";
private string _workingContent = null;
public bool UseWebsocket = false;
@ -135,34 +142,37 @@ namespace BLAIzor.Services
}
public async Task<WebsiteContentModel> InitSite(string sessionId, int SiteId, string templateCollectionName, string menuList = "")
{
string currentUri = _navigationManager.Uri;
SiteInfo site = await _contentEditorService.GetSiteInfoByIdAsync(SiteId);
WebsiteContentModel siteModel = null;
siteModel = await _websiteContentLoaderService.LoadAllAsync(
site,
_qDrantService.GetPointsFromQdrantAsyncByPointIds
);
_scopedContentService.WebsiteContentModel = siteModel;
_scopedContentService.SessionId = sessionId;
public async Task<WebsiteContentModel> InitSite(string sessionId, SiteInfo site, string templateCollectionName, string menuList = "")
{
await _logger.InfoAsync($"InitSite: method called", templateCollectionName);
string currentUri = _navigationManager.Uri;
if (site.DefaultLanguage != null)
{
_scopedContentService.WebsiteDefaultLanguage = site.DefaultLanguage;
}
WebsiteContentModel siteModel = await _cacheService.UpdateContentCache(sessionId, site.Id);
//_scopedContentService.SessionId = sessionId;
_scopedContentService.AvailableTemplateSnippets = await GetSnippetsForDisplay(sessionId, templateCollectionName);
return siteModel;
}
public async Task GetChatGptWelcomeMessage(string sessionId, int SiteId, string templateCollectionName, string menuList = "")
{
var siteModel = await InitSite(sessionId, SiteId, templateCollectionName, menuList);
await _logger.InfoAsync($"GetChatGptWelcomeMessage: method called", templateCollectionName);
SiteInfo site = await _contentEditorService.GetSiteInfoByIdAsync(SiteId);
var siteModel = await InitSite(sessionId, site, templateCollectionName, menuList);
//_apiKey = GetApiKey();
//List<string> qdrantPoint = await _qDrantService.GetContentAsync(SiteId, _scopedContentService.WebsiteContent.Items.FirstOrDefault());
string extractedText = "";
//nullcheck
if(siteModel != null)
if (siteModel != null)
{
if(siteModel.ContentItems != null && siteModel.ContentItems.Count > 0)
await _logger.InfoAsync($"GetChatGptWelcomeMessage: sitemodel not null", siteModel.SiteInfoId.ToString());
if (siteModel.ContentItems != null && siteModel.ContentItems.Count > 0)
{
if (siteModel.ContentItems.OrderBy(sm => sm.ContentItem.Id).FirstOrDefault().VectorPoints != null)
{
@ -175,7 +185,7 @@ namespace BLAIzor.Services
{
extractedText = "Content not available";
}
}
}
}
else
{
@ -205,17 +215,30 @@ namespace BLAIzor.Services
AiProvider = GetAiSettings();
if (AiProvider == "cerebras")
{
await _cerebrasAPIService.GetCerebrasStreamedResponse(sessionId, systemMessage, userMessage);
var result = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userMessage);
var fixedForColons = TextHelper.FixJsonWithoutAI(result);
OnContentReceived?.Invoke(sessionId, fixedForColons, null);
}
else if (AiProvider == "chatgpt")
{
await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage);
//var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userMessage);
//var fixedForColons = TextHelper.FixJsonWithoutAI(result);
//OnContentReceived?.Invoke(sessionId, fixedForColons, null);
}
else if (AiProvider == "deepseek")
{
//await _deepSeekApiService.GetChatGPTStreamedResponse(systemMessage, userMessage);
}
if (AiProvider == "kimi")
{
var result = await _kimiApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userMessage);
var fixedForColons = TextHelper.FixJsonWithoutAI(result);
OnContentReceived?.Invoke(sessionId, fixedForColons, null);
}
}
else
{
@ -234,17 +257,18 @@ namespace BLAIzor.Services
public async Task ProcessUserIntent(string sessionId, string userPrompt, int siteId, int templateId, string contentCollectionName, string menuList = "")
{
await _logger.InfoAsync($"ProcessUserIntent: method called", $"sessionId: {sessionId}, userprompt: {userPrompt}, siteId: {siteId}, templateId: {templateId}, contentCollectionName: {contentCollectionName}");
//Console.WriteLine($"SITE ID: {siteId}");
OnStatusChangeReceived?.Invoke(sessionId, "Understanding your request...");
// Get JSON result based on siteId presence
string resultJson = await GetJsonResultFromQuery(sessionId, siteId, userPrompt);
//Console.WriteLine(resultJson);
var baseResult = await ValidateAndFixJson<ChatGPTResultBase>(resultJson, FixJsonWithAI);
//var baseResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTResultBase>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var fixedResult = System.Text.Json.JsonSerializer.Serialize(baseResult);
if (baseResult == null)
{
@ -252,6 +276,7 @@ namespace BLAIzor.Services
return;
}
var fixedResult = System.Text.Json.JsonSerializer.Serialize(baseResult);
OnStatusChangeReceived?.Invoke(sessionId, "Making a decision");
// Process result based on type
@ -302,7 +327,7 @@ namespace BLAIzor.Services
{
//do we have it cached?
var contentId = requestedMenu.ContentItemId;
if(_scopedContentService.WebsiteContentModel != null)
if (_scopedContentService.WebsiteContentModel != null)
{
if (_scopedContentService.WebsiteContentModel.ContentItems.Any(x => x.ContentItem.Id == contentId))
{
@ -317,7 +342,7 @@ namespace BLAIzor.Services
extractedText = thisContent.ContentItem.Content;
}
}
}
else
{
//get vector of menuItem
@ -325,7 +350,7 @@ namespace BLAIzor.Services
if (requestedMenu.ContentGroupId != null)
{
//here we need something that turns the menuitem into contentitem... oh wait we have that..
var contentItem = await _contentEditorService.GetContentItemByIdAsync((int)requestedMenu.ContentItemId);
var vectorList = contentItem.Chunks.ToList();
//var vectors = await _contentEditorService.GetPointIdsByContentGroupIdAsync((int)requestedMenu.ContentGroupId);
@ -348,7 +373,7 @@ namespace BLAIzor.Services
//error, no contentgroup yet
}
//TODO
}
}
@ -362,7 +387,7 @@ namespace BLAIzor.Services
public async Task ProcessContentRequest(string sessionId, string requestedMenu, int siteId, int templateId, string contentCollectionName, string menuList = "", bool forceUnmodified = false)
{
await _logger.InfoAsync($"ProcessContentRequest: method called", $"sessionId: {sessionId}, requestedMenu: {requestedMenu}, siteId: {siteId}, templateId: {templateId}, contentCollectionName: {contentCollectionName}");
//Console.Write($"\n\n SessionId: {sessionId}\n\n");
//string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _contentService.SelectedDocument);
@ -397,7 +422,7 @@ namespace BLAIzor.Services
//PointId[] pointArray = new PointId[intArray.Length];
if (pointList.Count > 0)
{
for(int i=0; i<pointList.Count; i++)
for (int i = 0; i < pointList.Count; i++)
{
extractedText += $"{pointList[i].Name}: {pointList[i].Content}";
}
@ -438,22 +463,24 @@ namespace BLAIzor.Services
private async Task ProcessMethodResult(string sessionId, string resultJson)
{
var fixedResult = await ValidateAndFixJson<ChatGPTMethodResult>(resultJson, FixJsonWithAI);
//var methodResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTMethodResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (fixedResult != null)
await _logger.InfoAsync($"ProcessMethodResult: method called", $"sessionId: {sessionId}, json: {resultJson}");
//var fixedResult = await ValidateAndFixJson<ChatGPTMethodResult>(resultJson, FixJsonWithAI); //ANOTHER fixing is useless, json is from fixed format
var methodResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTMethodResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (methodResult != null)
{
OnStatusChangeReceived?.Invoke(sessionId, "Initiating the task you requested");
await DisplayHtml(sessionId, fixedResult.Text, fixedResult.MethodToCall, fixedResult.Parameter);
await DisplayHtml(sessionId, methodResult.Text, methodResult.MethodToCall, methodResult.Parameter);
}
}
private async Task ProcessTextResult(string sessionId, string pageTitle, string resultJson, int templateId, string collectionName)
{
var fixedResult = await ValidateAndFixJson<ChatGPTTextResult>(resultJson, FixJsonWithAI);
//var textResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTTextResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (fixedResult != null)
await _logger.InfoAsync($"ProcessTextResult: method called", $"sessionId: {sessionId}, json: {resultJson}, pageTitle: {pageTitle}, templateId: {templateId}, collectionName: {collectionName}");
//var fixedResult = await ValidateAndFixJson<ChatGPTTextResult>(resultJson, FixJsonWithAI); //ANOTHER fixing is useless, json is from fixed format
var textResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTTextResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (textResult != null)
{
string contentJson = await GetContentFromQuery(sessionId, fixedResult.Text, _workingContent);
string contentJson = await GetContentFromQuery(sessionId, textResult.Text, _workingContent);
//Console.Write("\r \n ProcessTextResult: Content: " + contentJson + "\r \n");
await ProcessContent(sessionId, pageTitle, contentJson, templateId, collectionName);
}
@ -461,6 +488,7 @@ namespace BLAIzor.Services
public async Task<T?> ValidateAndFixJson<T>(string json, Func<string, Task<string>> aiFixer)
{
await _logger.InfoAsync($"ValidateAndFixJson: method called", $"json: {json}");
try
{
return System.Text.Json.JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
@ -470,8 +498,8 @@ namespace BLAIzor.Services
}
catch (Exception ex)
{
//Console.WriteLine($"❌ JSON parse failed: {ex.Message}");
await _logger.ErrorAsync($"ValidateAndFixJson: failed first!", $"message: {ex.Message}");
var prompt = BuildJsonFixPrompt(json, ex.Message, typeof(T).Name);
var fixedJson = await aiFixer(prompt);
@ -484,7 +512,8 @@ namespace BLAIzor.Services
}
catch (Exception ex2)
{
//Console.WriteLine($"❌ AI-fix parse failed: {ex2.Message}");
await _logger.ErrorAsync($"ValidateAndFixJson: failed again!", $"❌ AI-fix parse failed: {ex2.Message}");
return default;
}
}
@ -509,7 +538,7 @@ namespace BLAIzor.Services
}
else { return ""; }
//return await _openAIApiService.GetSimpleChatGPTResponseNoSession("You are a JSON-fixing assistant.", prompt);
}
private string BuildJsonFixPrompt(string json, string errorMessage, string targetTypeName)
@ -532,17 +561,19 @@ namespace BLAIzor.Services
private async Task ProcessExaminationResult(string sessionId, string resultJson, int templateId, string collectionName)
{
var fixedResult = await ValidateAndFixJson<ChatGPTExaminationResult>(resultJson, FixJsonWithAI);
//var explanationResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTExaminationResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (fixedResult != null)
await _logger.InfoAsync($"ProcessExaminationResult: method called", $"sessionId: {sessionId}, json: {resultJson}, templateId: {templateId}, collectionName: {collectionName}");
//var fixedResult = await ValidateAndFixJson<ChatGPTExaminationResult>(resultJson, FixJsonWithAI);
var explanationResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTExaminationResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (explanationResult != null)
{
string contentJson = await GetExplanationFromQuery(sessionId, fixedResult.Text, _scopedContentService.CurrentDOM);
string contentJson = await GetExplanationFromQuery(sessionId, explanationResult.Text, _scopedContentService.CurrentDOM);
await ProcessContent(sessionId, "Examination", contentJson, templateId, collectionName);
}
}
private async Task ProcessContent(string sessionId, string pageTitle, string contentJson, int templateId, string contentCollectionName)
{
await _logger.InfoAsync($"ProcessContent: method called", $"sessionId: {sessionId}, pageTitle: {pageTitle}, json: {contentJson}, templateId: {templateId}, contentCollectionName: {contentCollectionName}");
try
{
var fixedResult = await ValidateAndFixJson<ChatGPTContentResult>(contentJson, FixJsonWithAI);
@ -559,7 +590,7 @@ namespace BLAIzor.Services
//We have the text all available now, let's pass it to the voice generator
//TODO modify photos handling, move audio generation to the layout area
string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(fixedResult.Text);
string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(fixedResult.Text, _scopedContentService.SelectedLanguage);
Console.WriteLine(removedNumbers);
OnTextContentAvailable?.Invoke(sessionId, removedNumbers);
//List<HtmlSnippet> snippets = await GetSnippetsForDisplay(sessionId, collectionName);
@ -581,6 +612,7 @@ namespace BLAIzor.Services
//passing menuitem further
private async Task ProcessContent(string sessionId, MenuItem requestedMenu, string contentJson, int templateId, string collectionName)
{
await _logger.InfoAsync($"ProcessContent: method called", $"sessionId: {sessionId}, menuItem: {requestedMenu.Name}, json: {contentJson}, templateId: {templateId}, contentCollectionName: {collectionName}");
try
{
var fixedResult = await ValidateAndFixJson<ChatGPTContentResult>(contentJson, FixJsonWithAI);
@ -597,7 +629,7 @@ namespace BLAIzor.Services
//We have the text all available now, let's pass it to the voice generator
//TODO modify photos handling, move audio generation to the layout area
string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(fixedResult.Text);
string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(fixedResult.Text, _scopedContentService.SelectedLanguage);
Console.WriteLine(removedNumbers);
OnTextContentAvailable?.Invoke(sessionId, removedNumbers);
//List<HtmlSnippet> snippets = await GetSnippetsForDisplay(sessionId, collectionName);
@ -618,6 +650,7 @@ namespace BLAIzor.Services
private async Task ProcessErrorResult(string sessionId, string resultJson)
{
await _logger.InfoAsync($"ProcessErrorResult: method called", $"sessionId: {sessionId}, json: {resultJson}");
var errorResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTErrorResult>(resultJson);
if (errorResult != null)
{
@ -634,6 +667,7 @@ namespace BLAIzor.Services
/// <returns></returns>
public async Task<string> GetContentFromQuery(string sessionId, string userPrompt, string content = null, bool forceUnmodified = false)
{
await _logger.InfoAsync($"GetContentFromQuery: method called", $"sessionId: {sessionId}, userPrompt: {userPrompt}, content: {content}, forceUnmodified: {forceUnmodified}");
string extractedText;
if (content == null)
{
@ -803,6 +837,7 @@ namespace BLAIzor.Services
public async Task<string> GetJsonResultFromQuery(string sessionId, int siteId, string userPrompt)
{
await _logger.InfoAsync($"GetJsonResultFromQuery: method called", $"SessionId: {sessionId}, userPrompt: {userPrompt}, siteId: {siteId}");
//string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _contentService.SelectedDocument);
//_apiKey = GetApiKey();
//start with embeddings
@ -831,13 +866,13 @@ namespace BLAIzor.Services
//have to get from qdrant
var pointList = await _qDrantService.QueryContentAsync(site.VectorCollectionName, vector, 3);
if (pointList.Count > 0)
{
for (int i = 0; i < pointList.Count; i++)
{
extractedText += pointList[i].Name + ": " + pointList[i].Content + ", ";
}
}
}
else
@ -868,7 +903,7 @@ namespace BLAIzor.Services
//string extractedText = WordFileReader.ExtractText(rootpath);
//Console.Write("GetJSONResult called!");
string systemMessage = AiPrompts.ContentProcessing.GetSystemMessageForJsonResultDecision(_scopedContentService.SelectedLanguage, extractedText, _scopedContentService.CurrentDOM);
string systemMessage = AiPrompts.ContentProcessing.GetSystemMessageForJsonResultDecision(_scopedContentService.SelectedLanguage, extractedText, _scopedContentService.CurrentDOM);
string interMediateResult = string.Empty;
if (!UseWebsocket)
@ -900,6 +935,7 @@ namespace BLAIzor.Services
public async Task<List<HtmlSnippet>> GetSnippetsForDisplay(string sessionId, string collectionName)
{
await _logger.InfoAsync($"GetSnippetsForDisplay: method called", $"SessionId: {sessionId}, collectionName: {collectionName}");
_apiKey = GetApiKey();
OnStatusChangeReceived?.Invoke(sessionId, "Looking up the UI template elements for you");
//string availableSnippetList = "";
@ -937,6 +973,7 @@ namespace BLAIzor.Services
//for textresult and errorresult
public async Task DisplayHtml(string sessionId, string pageTitle, LayoutPlan layoutPlan, List<HtmlSnippet> htmlToUse, string[]? topics = null, Dictionary<string, string>? photos = null)
{
await _logger.InfoAsync($"DisplayHtml: method called", $"SessionId: {sessionId}, pageTitle: {pageTitle}, layoutPlan: {layoutPlan.Blocks.Count}, htmlToUse: {htmlToUse.Count}, topics: {topics?.Length}, photos: {photos?.Count}");
//Console.Write($"\n SessionId: {sessionId} \n");
OnStatusChangeReceived?.Invoke(sessionId, "Casting spells to draw customized UI");
@ -944,8 +981,9 @@ namespace BLAIzor.Services
//Console.WriteLine($"DISPLAYHTML Topics: {topics} \n\n");
string systemMessage = AiPrompts.HtmlRendering.GetHtmlRenderingSystemPromptForTextAndErrorResult(_scopedContentService.SelectedLanguage, pageTitle, htmlToUse, photos, topics);
string userMessage = AiPrompts.HtmlRendering.HtmlRenderingUserPromptForTextAndErrorResult;
string assistantMessage = AiPrompts.HtmlRendering.GetHtmlRenderingAssistantMessageForTextAndErrorResult(layoutPlan);
//string userMessage = AiPrompts.HtmlRendering.HtmlRenderingUserPromptForTextAndErrorResult;
//string assistantMessage = AiPrompts.HtmlRendering.GetHtmlRenderingAssistantMessageForTextAndErrorResult(layoutPlan);
string userMessage = AiPrompts.HtmlRendering.HtmlRenderingUserPromptForTextAndErrorResult + AiPrompts.HtmlRendering.GetHtmlRenderingAssistantMessageForTextAndErrorResult(layoutPlan);
//string assistantMessage = "`Provided layout plan, that contains the text to be displayed as HTML`:";
@ -961,14 +999,14 @@ namespace BLAIzor.Services
if (AiProvider == "cerebras")
{
//await _cerebrasAPIService.GetCerebrasStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
var result = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
var result = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userMessage, "", -1);
var fixedForColons = TextHelper.FixJsonWithoutAI(result);
OnContentReceived?.Invoke(sessionId, fixedForColons, null);
}
else if (AiProvider == "chatgpt")
{
//await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userMessage, "", -1);
var fixedForColons = TextHelper.FixJsonWithoutAI(result);
OnContentReceived?.Invoke(sessionId, fixedForColons, null);
}
@ -983,12 +1021,13 @@ namespace BLAIzor.Services
}
else
{
await _openAIRealtimeService.GetChatGPTResponseAsync(sessionId, systemMessage, userMessage + assistantMessage);
await _openAIRealtimeService.GetChatGPTResponseAsync(sessionId, systemMessage, userMessage, "");
}
}
public async Task DisplayHtmlForMenu(string sessionId, MenuItem requestedMenu, LayoutPlan layoutPlan, List<HtmlSnippet> htmlToUse, string[]? topics = null, Dictionary<string, string>? photos = null)
{
await _logger.InfoAsync($"DisplayHtmlForMenu: method called", $"SessionId: {sessionId}, requestedMenu: {requestedMenu.Name}, layoutPlan: {layoutPlan.Blocks.Count}, htmlToUse: {htmlToUse.Count}, topics: {topics?.Length}, photos: {photos?.Count}");
//Console.Write($"\n SessionId: {sessionId} \n");
OnStatusChangeReceived?.Invoke(sessionId, "Casting spells to draw customized UI");
@ -1042,13 +1081,15 @@ namespace BLAIzor.Services
public async Task<LayoutPlan?> DisplayLayoutPlanFromContent(string sessionId, string pageTitle, string interMediateResult, List<HtmlSnippet> htmlToUse, string[]? topics = null, Dictionary<string, string>? photos = null)
{
//Console.Write($"\n SessionId: {sessionId} \n");
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: method called for methodResult", sessionId.ToString());
OnStatusChangeReceived?.Invoke(sessionId, "Planning layout based on the provided content");
//Console.WriteLine($"LAYOUTBUILDER Text: {interMediateResult}\n\n");
//Console.WriteLine($"LAYOUTBUILDER Snippets: {htmlToUse.Count}\n\n");
//Console.WriteLine($"LAYOUTBUILDER Photos: {photos} \n\n");
//Console.WriteLine($"LAYOUTBUILDER Topics: {topics} \n\n");
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: starting text", $"{interMediateResult}");
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: starting snippets count", htmlToUse.Count.ToString());
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: starting photos count", photos.Count.ToString());
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: starting topics count", topics.Length.ToString());
string systemMessage = AiPrompts.LayoutPlanning.GetLayoutPlanningSystemPrompt(htmlToUse, photos, topics);
string userMessage = AiPrompts.LayoutPlanning.GetLayoutPlanningUserPrompt(interMediateResult, pageTitle, photos);
@ -1093,7 +1134,7 @@ namespace BLAIzor.Services
{
//Console.WriteLine("AI Response:");
//Console.WriteLine(aiResponse);
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: ai Response", $"{aiResponse}");
aiResponse = TextHelper.FixJsonWithoutAI(aiResponse);
aiResponse = TextHelper.RemoveTabs(aiResponse);
layoutPlan = System.Text.Json.JsonSerializer.Deserialize<LayoutPlan>(aiResponse, new JsonSerializerOptions
@ -1101,42 +1142,47 @@ namespace BLAIzor.Services
PropertyNameCaseInsensitive = true
});
if (layoutPlan?.Blocks == null || layoutPlan.Blocks.Any(b => string.IsNullOrEmpty(b.Type) || string.IsNullOrEmpty(b.RawContent)))
//if (layoutPlan?.Blocks == null || layoutPlan.Blocks.Any(b => string.IsNullOrEmpty(b.Type) || string.IsNullOrEmpty(b.RawContent)))
if (layoutPlan?.Blocks == null || layoutPlan.Blocks.Any(b => string.IsNullOrEmpty(b.Type)))
{
//try to fix with AI)
//Console.WriteLine("Trying to fix with AI.");
await _logger.WarnAsync($"DisplayLayoutPlanFromContent: trying to fix with AI", $"{aiResponse}");
layoutPlan = await ValidateAndFixJson<LayoutPlan>(aiResponse, FixJsonWithAI);
if (layoutPlan?.Blocks == null || layoutPlan.Blocks.Any(b => string.IsNullOrEmpty(b.Type) || string.IsNullOrEmpty(b.RawContent)))
{
Console.WriteLine("Invalid block structure in response.");
await _logger.ErrorAsync($"DisplayLayoutPlanFromContent: Invalid block structure in response.", $"{layoutPlan.Blocks.Count}");
layoutPlan = null;
}
}
}
catch (Exception ex)
{
Console.WriteLine("Deserialization failed: " + ex.Message);
Console.WriteLine("Deserialization failed on json: " + aiResponse);
await _logger.ErrorAsync($"DisplayLayoutPlanFromContent: ai Response", ex.Message);
layoutPlan = null;
}
if (layoutPlan == null)
{
retry++;
Console.WriteLine("Retrying due to invalid format...");
await _logger.WarnAsync($"DisplayLayoutPlanFromContent: Retrying due to invalid format...", $"{aiResponse}");
}
}
foreach (var block in layoutPlan.Blocks)
{
Console.WriteLine($"{block.Order}, {block.Type}, {block.PreferredSnippetId}");
if(block.ContentMap!=null && block.ContentMap.Count > 0)
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: reading blocks", $"{block.Order}, {block.Type}, {block.PreferredSnippetId}");
Console.WriteLine($"{block.Order}, {block.Type}, {block.PreferredSnippetId}");
if (block.ContentMap != null && block.ContentMap.Count > 0)
{
foreach (var key in block.ContentMap.Keys)
{
Console.WriteLine($"key {key} : value: {block.ContentMap[key]}");
await _logger.InfoAsync($"DisplayLayoutPlanFromContent: reading ContentMap", $"key {key} : value: {block.ContentMap[key]}");
//Console.WriteLine($"key {key} : value: {block.ContentMap[key]}");
}
}
}
return layoutPlan;
}
@ -1146,6 +1192,7 @@ namespace BLAIzor.Services
//for methodResult
public async Task DisplayHtml(string sessionId, string interMediateResult, string methodToCall = "", string methodParameter = "")//, string[]? topics = null)
{
await _logger.InfoAsync($"DisplayHtml: method called for methodResult", sessionId.ToString());
//Console.Write($"\n SessionId: {sessionId} \n");
OnStatusChangeReceived?.Invoke(sessionId, "Casting spells to draw customized UI");
@ -1162,6 +1209,7 @@ namespace BLAIzor.Services
AiProvider = GetAiSettings();
if (AiProvider == "cerebras")
{
await _logger.InfoAsync($"DisplayHtml: streamed response", sessionId.ToString());
await _cerebrasAPIService.GetCerebrasStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
}
else if (AiProvider == "chatgpt")

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

@ -1,5 +1,6 @@
using BLAIzor.Data;
using BLAIzor.Models;
using Microsoft.DotNet.Scaffolding.Shared;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Client;
@ -9,24 +10,50 @@ namespace BLAIzor.Services
{
//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 ContentEditorAIService(/*AIService aiService,*/ OpenAIApiService openAIApiService, /*ApplicationDbContext context,*/ QDrantService qDrantService, HtmlSnippetProcessor htmlSnippetProcessor, IServiceScopeFactory serviceScopeFactory, ScopedContentService scopedContentService, IConfiguration? configuration, ContentEditorService contentEditorService)
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;
}
@ -34,27 +61,93 @@ namespace BLAIzor.Services
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<List<string>> GetMenuSuggestionsAsync(string sessionId, string prompt)
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.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 in creating a website. Do not generate html, just plain text.";
var result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, 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

View File

@ -3,32 +3,55 @@ using BLAIzor.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Client;
using BLAIzor.Helpers;
using Qdrant.Client.Grpc;
using System.Collections;
namespace BLAIzor.Services
{
public class ContentEditorService
{
//private readonly AIService _aiService;
#region Private Fields
private readonly OpenAIApiService _openAIApiService;
//private readonly ApplicationDbContext _context;
private readonly QDrantService _qDrantService;
private readonly HtmlSnippetProcessor _htmlSnippetProcessor;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ScopedContentService _scopedContentService;
private readonly OpenAIEmbeddingService _openAIEmbeddingService;
private readonly LocalEmbeddingService _localEmbeddingService;
private readonly ISimpleLogger _logger;
public static IConfiguration? _configuration;
public ContentEditorService(/*AIService aiService,*/ OpenAIApiService openAIApiService, /*ApplicationDbContext context,*/ QDrantService qDrantService, OpenAIEmbeddingService openAIEmbeddingService, LocalEmbeddingService localEmbeddingService, HtmlSnippetProcessor htmlSnippetProcessor, IServiceScopeFactory serviceScopeFactory, ScopedContentService scopedContentService, IConfiguration? configuration)
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="ContentEditorService"/> class.
/// </summary>
/// <param name="openAIApiService">The OpenAI API service.</param>
/// <param name="qDrantService">The Qdrant service.</param>
/// <param name="openAIEmbeddingService">The OpenAI embedding service.</param>
/// <param name="localEmbeddingService">The local embedding service.</param>
/// <param name="htmlSnippetProcessor">The HTML snippet processor.</param>
/// <param name="serviceScopeFactory">The service scope factory for creating database contexts.</param>
/// <param name="scopedContentService">The scoped content service.</param>
/// <param name="configuration">The application configuration.</param>
public ContentEditorService(OpenAIApiService openAIApiService,
QDrantService qDrantService,
OpenAIEmbeddingService openAIEmbeddingService,
LocalEmbeddingService localEmbeddingService,
HtmlSnippetProcessor htmlSnippetProcessor,
IServiceScopeFactory serviceScopeFactory,
ScopedContentService scopedContentService,
IConfiguration? configuration,
ISimpleLogger logger)
{
//_aiService = aiService;
_openAIApiService = openAIApiService;
//_context = context;
_qDrantService = qDrantService;
_openAIEmbeddingService = openAIEmbeddingService;
_localEmbeddingService = localEmbeddingService;
@ -36,84 +59,132 @@ namespace BLAIzor.Services
_serviceScopeFactory = serviceScopeFactory;
_scopedContentService = scopedContentService;
_configuration = configuration;
_logger = logger;
}
#endregion
#region Private Helper Methods
/// <summary>
/// Retrieves the AI embedding service setting from the configuration.
/// </summary>
/// <returns>The name of the embedding service (e.g., "local", "openai").</returns>
private string GetAiEmbeddingSettings() =>
_configuration?.GetSection("AiSettings")?.GetValue<string>("EmbeddingService") ?? string.Empty;
// CRUD methods for MenuItems
/// <summary>
/// Removes content chunks and their corresponding Qdrant entries by their IDs.
/// </summary>
/// <param name="chunkIds">A list of content chunk IDs to remove.</param>
/// <param name="collectionName">The name of the Qdrant collection.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task RemoveChunksAndQdrantEntriesByIdsAsync(List<int> chunkIds, string collectionName)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Create a new MenuItem
var chunks = await db.ContentChunks
.Where(c => chunkIds.Contains(c.Id))
.ToListAsync();
var pointIds = chunks.Select(c => Guid.Parse(c.QdrantPointId)).ToArray();
await _qDrantService.DeletePointsAsync(pointIds, collectionName);
db.ContentChunks.RemoveRange(chunks);
await db.SaveChangesAsync();
}
/// <summary>
/// Generates a unique vector collection name for a given site.
/// </summary>
/// <param name="siteInfo">The site information.</param>
/// <returns>A generated vector collection name.</returns>
public string GetGeneratedVectorCollectionName(SiteInfo siteInfo)
{
var safeName = siteInfo.SiteName?.ToLower().Replace(" ", "_").Replace("-", "_");
return $"site_{safeName}_{Guid.NewGuid().ToString().Substring(0, 8)}";
}
#endregion
#region MenuItem CRUD Operations
/// <summary>
/// Adds a new menu item to the database.
/// </summary>
/// <param name="menuItem">The menu item to add.</param>
/// <returns>The added menu item with its generated ID.</returns>
public async Task<MenuItem> AddMenuItemAsync(MenuItem menuItem)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.MenuItems.AddAsync(menuItem);
await _context.SaveChangesAsync();
return result.Entity;
}
}
/// <summary>
/// Adds items as list
/// Adds multiple menu items to the database.
/// </summary>
/// <param name="menuItems"></param>
/// <returns>the number of modified rows</returns>
/// <param name="menuItems">A list of menu items to add.</param>
/// <returns>The number of state entries written to the database.</returns>
public async Task<int> AddMenuItemsAsync(List<MenuItem> menuItems)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
_context.MenuItems.AddRange(menuItems);
var result = await _context.SaveChangesAsync();
return result;
}
}
// Get all MenuItems for a specific SiteInfo
/// <summary>
/// Retrieves all menu items for a specific site.
/// </summary>
/// <param name="siteInfoId">The ID of the site.</param>
/// <returns>A list of menu items belonging to the specified site.</returns>
public async Task<List<MenuItem>> GetMenuItemsBySiteIdAsync(int siteInfoId)
{
await _logger.InfoAsync($"GetMenuItemsBySiteIdAsync: method called", siteInfoId.ToString());
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.MenuItems
.Where(m => m.SiteInfoId == siteInfoId)
.ToListAsync();
return result;
}
}
//GET MENUS WITH CHILDREN
/// <summary>
/// Retrieves all top-level menu items for a specific site, including their children.
/// </summary>
/// <param name="siteInfoId">The ID of the site.</param>
/// <returns>A list of top-level menu items with their associated children.</returns>
public async Task<List<MenuItem>> GetMenuItemsBySiteIdWithChildrenAsync(int siteInfoId)
{
await _logger.InfoAsync($"GetMenuItemsBySiteIdWithChildrenAsync: method called", siteInfoId.ToString());
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.MenuItems
.Where(m => m.SiteInfoId == siteInfoId && m.ParentId == null)
.Include(m => m.Children)
.ToListAsync();
return result;
}
}
// Update an existing MenuItem
/// <summary>
/// Updates an existing menu item in the database.
/// </summary>
/// <param name="menuItem">The menu item with updated information.</param>
/// <returns>The updated menu item.</returns>
/// <exception cref="Exception">Thrown if the menu item is not found.</exception>
public async Task<MenuItem> UpdateMenuItemAsync(MenuItem menuItem)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -132,21 +203,21 @@ namespace BLAIzor.Services
existingMenuItem.SortOrder = menuItem.SortOrder;
existingMenuItem.ShowInMainMenu = menuItem.ShowInMainMenu;
// No need for _context.MenuItems.Update(existingMenuItem);
await _context.SaveChangesAsync();
return existingMenuItem;
}
// Delete a MenuItem
/// <summary>
/// Deletes a menu item from the database.
/// </summary>
/// <param name="menuItemId">The ID of the menu item to delete.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="Exception">Thrown if the menu item is not found.</exception>
public async Task DeleteMenuItemAsync(int menuItemId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var menuItem = await _context.MenuItems.FindAsync(menuItemId);
if (menuItem == null)
{
@ -155,32 +226,39 @@ namespace BLAIzor.Services
_context.MenuItems.Remove(menuItem);
await _context.SaveChangesAsync();
}
}
#endregion
#region ContentGroup Operations
/// <summary>
/// Retrieves the first ContentGroup associated with the given SiteInfoId.
/// Returns null if not found.
/// </summary>
/// <param name="siteInfoId">The ID of the site information.</param>
/// <returns>The first ContentGroup found, or null if none exists.</returns>
public async Task<ContentGroup?> GetContentGroupBySiteInfoIdAsync(int siteInfoId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.ContentGroups
.Where(cg => cg.SiteInfoId == siteInfoId)
.OrderBy(cg => cg.Id) // or by LastUpdated, Name, etc.
.OrderBy(cg => cg.Id)
.FirstOrDefaultAsync();
return result;
}
}
/// <summary>
/// Returns the first ContentGroup for a site (can be filtered by type or slug).
/// Returns a list of ContentGroups for a site, with optional filtering by type or slug.
/// </summary>
/// <param name="siteInfoId">The ID of the site information.</param>
/// <param name="type">Optional. The type of the content group to filter by.</param>
/// <param name="slug">Optional. The slug of the content group to filter by.</param>
/// <returns>A list of matching ContentGroups.</returns>
public async Task<List<ContentGroup>> GetContentGroupsBySiteInfoIdAsync(int siteInfoId, string? type = null, string? slug = null)
{
using (var scope = _serviceScopeFactory.CreateScope())
@ -205,6 +283,12 @@ namespace BLAIzor.Services
}
}
/// <summary>
/// Updates an existing content group in the database.
/// </summary>
/// <param name="updatedGroup">The content group with updated information.</param>
/// <returns>The updated content group.</returns>
/// <exception cref="Exception">Thrown if the content group is not found.</exception>
public async Task<ContentGroup> UpdateContentGroupByIdAsync(ContentGroup updatedGroup)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -228,6 +312,11 @@ namespace BLAIzor.Services
return existingGroup;
}
/// <summary>
/// Deletes a content group by its ID.
/// </summary>
/// <param name="contentGroupId">The ID of the content group to delete.</param>
/// <returns><c>true</c> if the content group was deleted, <c>false</c> otherwise.</returns>
public async Task<bool> DeleteContentGroupByIdAsync(int contentGroupId)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -243,6 +332,11 @@ namespace BLAIzor.Services
return true;
}
/// <summary>
/// Creates a new content group in the database.
/// </summary>
/// <param name="newGroup">The new content group to create.</param>
/// <returns>The newly created content group with its generated ID and timestamps.</returns>
public async Task<ContentGroup> CreateContentGroupAsync(ContentGroup newGroup)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -257,10 +351,12 @@ namespace BLAIzor.Services
return newGroup;
}
/// <summary>
/// Returns all ContentGroups for a site (can be filtered by type).
/// Returns all ContentGroups for a site, with optional filtering by type.
/// </summary>
/// <param name="siteInfoId">The ID of the site information.</param>
/// <param name="type">Optional. The type of the content group to filter by.</param>
/// <returns>A list of matching ContentGroups.</returns>
public async Task<List<ContentGroup>> GetAllContentGroupsBySiteInfoIdAsync(int siteInfoId, string? type = null)
{
using (var scope = _serviceScopeFactory.CreateScope())
@ -278,7 +374,17 @@ namespace BLAIzor.Services
}
}
public async Task<ContentItem> GetContentItemByIdAsync(int contentItemId, string? type = null)
#endregion
#region ContentItem Operations
/// <summary>
/// Retrieves a content item by its ID, optionally filtered by type.
/// </summary>
/// <param name="contentItemId">The ID of the content item.</param>
/// <param name="type">Optional. The type of the content item to filter by.</param>
/// <returns>The matching content item with its chunks, or null if not found.</returns>
public async Task<ContentItem?> GetContentItemByIdAsync(int contentItemId, string? type = null)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
@ -290,6 +396,11 @@ namespace BLAIzor.Services
}
}
/// <summary>
/// Retrieves a content item by its ID, including related ContentGroup, Chunks, and SiteInfo.
/// </summary>
/// <param name="id">The ID of the content item.</param>
/// <returns>The matching content item, or null if not found.</returns>
public async Task<ContentItem?> GetContentItemByIdAsync(int id)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -298,41 +409,25 @@ namespace BLAIzor.Services
return query.FirstOrDefault();
}
//public async Task<ContentItem> UpdateContentItemByIdAsync(ContentItem item)
//{
// using var scope = _serviceScopeFactory.CreateScope();
// var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// var existing = await db.ContentItems.FindAsync(item.Id);
// if (existing == null) throw new Exception("ContentItem not found.");
// // Update properties manually (or use AutoMapper if you prefer)
// existing.Title = item.Title;
// existing.Description = item.Description;
// existing.Content = item.Content;
// existing.Language = item.Language;
// existing.Tags = item.Tags;
// existing.IsPublished = item.IsPublished;
// existing.Version = item.Version;
// existing.LastUpdated = item.LastUpdated;
// await db.SaveChangesAsync();
// return existing;
//}
/// <summary>
/// Creates a new content item, chunks its content, generates embeddings, and inserts them into Qdrant.
/// </summary>
/// <param name="item">The content item to create.</param>
/// <param name="collectionName">The name of the Qdrant collection to insert into.</param>
/// <returns>The created content item.</returns>
/// <exception cref="Exception">Thrown if the saved ContentItem cannot be retrieved.</exception>
public async Task<ContentItem> CreateContentItemAsync(ContentItem item, string collectionName)
{
await _logger.InfoAsync($"CreateContentItemAsync: method called", item.Id.ToString());
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
item.CreatedAt = DateTime.UtcNow;
item.LastUpdated = DateTime.UtcNow;
// Save to DB first to get ContentItem.Id
db.ContentItems.Add(item);
await db.SaveChangesAsync();
// Get the full ContentItem with ContentGroup included (needed for SiteId)
var fullItem = await db.ContentItems
.Include(ci => ci.ContentGroup)
.FirstOrDefaultAsync(ci => ci.Id == item.Id);
@ -340,17 +435,15 @@ namespace BLAIzor.Services
if (fullItem == null)
throw new Exception("Failed to retrieve saved ContentItem.");
// 🧠 Chunking
if (!string.IsNullOrEmpty(item.Content))
{
var chunks = ChunkingHelper.SplitStructuredText(item.Content, 3000); // customize if needed
var chunks = ChunkingHelper.SplitStructuredText(item.Content, 3000);
var vectorizedChunks = await VectorizeChunksWithGuidsAsync(chunks, fullItem, collectionName);
// 🔗 Save chunk references
var chunkEntities = vectorizedChunks.Select((chunk, index) => new ContentChunk
{
ContentItemId = item.Id,
QdrantPointId = chunk.UId, // now using GUIDs
QdrantPointId = chunk.UId,
ChunkIndex = index,
CreatedAt = DateTime.UtcNow
}).ToList();
@ -362,9 +455,13 @@ namespace BLAIzor.Services
return item;
}
/// <summary>
/// Vectorizes a list of content chunks, generates unique GUIDs for each, and inserts them into Qdrant.
/// </summary>
/// <param name="chunks">The list of text chunks to vectorize.</param>
/// <param name="item">The content item associated with these chunks.</param>
/// <param name="collectionName">The name of the Qdrant collection.</param>
/// <returns>A list of <see cref="WebPageContent"/> representing the vectorized chunks.</returns>
public async Task<List<WebPageContent>> VectorizeChunksWithGuidsAsync(List<string> chunks, ContentItem item, string collectionName)
{
var result = new List<WebPageContent>();
@ -388,6 +485,7 @@ namespace BLAIzor.Services
result.Add(new WebPageContent
{
Id = Guid.Parse(uid),
UId = uid,
SiteId = item.ContentGroup.SiteInfoId,
Type = "content-item",
@ -403,14 +501,19 @@ namespace BLAIzor.Services
return result;
}
/// <summary>
/// Vectorizes a list of content chunks and inserts them into Qdrant.
/// </summary>
/// <param name="chunks">The list of text chunks to vectorize.</param>
/// <param name="item">The content item associated with these chunks.</param>
/// <param name="collectionName">The name of the Qdrant collection.</param>
/// <returns>A list of <see cref="WebPageContent"/> representing the vectorized chunks.</returns>
public async Task<List<WebPageContent>> VectorizeChunksAsync(List<string> chunks, ContentItem item, string collectionName)
{
var result = new List<WebPageContent>();
foreach (var (chunk, index) in chunks.Select((c, i) => (c, i)))
{
//var combinedText = $"{pageContent.Name}: {pageContent.Description} - {chunk}";
var combinedText = $"{chunk}";
float[] embedding = [];
@ -424,10 +527,6 @@ namespace BLAIzor.Services
embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText);
}
// Add data for batch insertion
var vector = embedding;
var uid = Guid.NewGuid();
var webChunk = new WebPageContent
@ -439,34 +538,22 @@ namespace BLAIzor.Services
Name = item.Title,
Description = item.Description,
Content = chunk,
Vectors = vector,
Vectors = embedding,
LastUpdated = DateTime.UtcNow
};
result.Add(webChunk);
}
await _qDrantService.QDrantInsertManyAsync(result, collectionName);
return result;
}
private async Task RemoveChunksAndQdrantEntriesByIdsAsync(List<int> chunkIds, string collectionName)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var chunks = await db.ContentChunks
.Where(c => chunkIds.Contains(c.Id))
.ToListAsync();
var pointIds = chunks.Select(c => Guid.Parse(c.QdrantPointId)).ToArray();
await _qDrantService.DeletePointsAsync(pointIds, collectionName);
db.ContentChunks.RemoveRange(chunks);
await db.SaveChangesAsync();
}
/// <summary>
/// Forces a re-chunking and re-embedding of all content items within a specified content group.
/// </summary>
/// <param name="contentGroupId">The ID of the content group to re-chunk.</param>
/// <param name="collectionName">The name of the Qdrant collection. If null, the existing collection name will be used.</param>
/// <returns><c>true</c> if the operation was successful, <c>false</c> otherwise.</returns>
public async Task<bool> ForceRechunkContentGroupAsync(int contentGroupId, string collectionName = null)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -488,12 +575,17 @@ namespace BLAIzor.Services
return true;
}
/// <summary>
/// Forces the recreation of a Qdrant collection for a given site, including re-chunking and re-embedding all content.
/// </summary>
/// <param name="siteInfoId">The ID of the site for which to recreate the collection.</param>
/// <returns><c>true</c> if the collection was successfully recreated and content re-synced, <c>false</c> otherwise.</returns>
/// <exception cref="Exception">Thrown if the new Qdrant collection cannot be created.</exception>
public async Task<bool> ForceRecreateQdrantCollectionAsync(int siteInfoId)
{
using var scope = _serviceScopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Load site + all content + chunks
var site = await db.SiteInfos
.Include(s => s.ContentGroups)
.ThenInclude(g => g.Items)
@ -505,21 +597,17 @@ namespace BLAIzor.Services
string oldCollectionName = site.VectorCollectionName;
// ❌ Delete old Qdrant collection
await _qDrantService.DeleteCollectionAsync(oldCollectionName);
// ✅ Generate and create new collection
string newCollectionName = GetGeneratedVectorCollectionName(site);
bool created = await _qDrantService.CreateQdrantCollectionAsync(newCollectionName);
if (!created)
throw new Exception($"Failed to create Qdrant collection '{newCollectionName}'.");
// Update and persist new collection name
site.VectorCollectionName = newCollectionName;
await UpdateSiteInfoAsync(site); // Assumes this method updates it properly
await UpdateSiteInfoAsync(site);
// ♻️ Rechunk and reinsert all content items using fresh DTOs
foreach (var group in site.ContentGroups)
{
foreach (var item in group.Items.ToList())
@ -545,8 +633,11 @@ namespace BLAIzor.Services
return true;
}
/// <summary>
/// Retrieves all content items belonging to a specific content group.
/// </summary>
/// <param name="contentGroupId">The ID of the content group.</param>
/// <returns>A list of content items in the specified group, ordered by last updated date descending.</returns>
public async Task<List<ContentItem>> GetContentItemsByGroupIdAsync(int contentGroupId)
{
using (var scope = _serviceScopeFactory.CreateScope())
@ -559,6 +650,38 @@ namespace BLAIzor.Services
}
}
/// <summary>
/// Retrieves all content items belonging to a specific content group.
/// </summary>
/// <param name="contentGroupId">The ID of the content group.</param>
/// <returns>A list of content items in the specified group, ordered by last updated date descending.</returns>
public async Task<List<ContentItem>> GetAllContentItemsBySiteIdAsync(int siteId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
//var site = await GetSiteInfoByIdAsync(siteId);
//List<ContentItem> contentList = new List<ContentItem>();
//foreach (var item in site.ContentGroups)
//{
// var contentItems = await GetContentItemsByGroupIdAsync(item.Id);
// contentList.AddRange(contentItems);
//}
//return contentList;
return await context.ContentItems
.Where(ci => ci.ContentGroup.SiteInfoId == siteId)
.Include(ci => ci.Chunks)
.ToListAsync();
}
}
/// <summary>
/// Creates a new content item in the database.
/// </summary>
/// <param name="newItem">The new content item to create.</param>
/// <returns>The newly created content item with its generated ID and timestamps.</returns>
public async Task<ContentItem?> CreateContentItemAsync(ContentItem newItem)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -573,6 +696,15 @@ namespace BLAIzor.Services
return newItem;
}
/// <summary>
/// Saves changes to a content item and synchronizes its chunks and embeddings with Qdrant.
/// If content has changed or re-chunking is forced, old chunks are removed and new ones are created and embedded.
/// </summary>
/// <param name="dto">The content item data transfer object with updated information.</param>
/// <param name="collectionName">The name of the Qdrant collection to sync with.</param>
/// <param name="forceRechunk">If set to <c>true</c>, forces re-chunking and re-embedding even if content hasn't changed.</param>
/// <returns>The updated and synchronized content item.</returns>
/// <exception cref="Exception">Thrown if the content item is not found.</exception>
public async Task<ContentItem> SaveAndSyncContentItemAsync(ContentItem dto, string collectionName, bool forceRechunk = false)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -586,10 +718,8 @@ namespace BLAIzor.Services
if (existing == null)
throw new Exception("ContentItem not found.");
// Determine if content has changed
bool contentChanged = existing.Content?.Trim() != dto.Content?.Trim();
// Update scalar fields only (don't touch navigation collections directly)
existing.Title = dto.Title;
existing.Description = dto.Description;
existing.Content = dto.Content;
@ -602,26 +732,22 @@ namespace BLAIzor.Services
{
existing.Version++;
// 🧹 Remove old chunks (track-safe)
var chunkIds = existing.Chunks.Select(c => c.Id).ToList();
if (chunkIds.Any())
{
await RemoveChunksAndQdrantEntriesByIdsAsync(chunkIds, collectionName);
// Detach removed chunks from current EF context to avoid SaveChanges conflict
foreach (var chunk in existing.Chunks.ToList())
{
db.Entry(chunk).State = EntityState.Detached;
}
existing.Chunks.Clear(); // avoid tracking conflicts
existing.Chunks.Clear();
}
// 🔪 Chunk and embed again
var newChunks = ChunkingHelper.SplitStructuredText(existing.Content, 3000);
var webChunks = await VectorizeChunksAsync(newChunks, existing, collectionName);
// 💾 Save chunk metadata in SQL
var chunkEntities = webChunks.Select((w, i) => new ContentChunk
{
ContentItemId = existing.Id,
@ -637,7 +763,11 @@ namespace BLAIzor.Services
return existing;
}
/// <summary>
/// Groups content items by their content group type.
/// </summary>
/// <param name="model">The website content model containing content items.</param>
/// <returns>A dictionary where the key is the content group type and the value is a list of content item models.</returns>
public static Dictionary<string, List<ContentItemModel>> GroupContentItemsByType(WebsiteContentModel model)
{
return model.ContentItems
@ -645,8 +775,11 @@ namespace BLAIzor.Services
.ToDictionary(g => g.Key, g => g.ToList());
}
/// <summary>
/// Deletes a content item by its ID.
/// </summary>
/// <param name="id">The ID of the content item to delete.</param>
/// <returns><c>true</c> if the content item was deleted, <c>false</c> otherwise.</returns>
public async Task<bool> DeleteContentItemByIdAsync(int id)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -663,11 +796,13 @@ namespace BLAIzor.Services
return false;
}
/// <summary>
/// Retrieves the Qdrant point IDs associated with content chunks belonging to a specific content group.
/// </summary>
/// <param name="contentGroupId">The ID of the content group.</param>
/// <returns>An array of Qdrant point IDs (represented as chunk indices).</returns>
public async Task<int[]> GetPointIdsByContentGroupIdAsync(int contentGroupId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
@ -678,21 +813,20 @@ namespace BLAIzor.Services
}
}
#endregion
public string GetGeneratedVectorCollectionName(SiteInfo siteInfo)
{
var safeName = siteInfo.SiteName?.ToLower().Replace(" ", "_").Replace("-", "_");
return $"site_{safeName}_{Guid.NewGuid().ToString().Substring(0, 8)}";
}
#region SiteInfo Operations
/// <summary>
/// Retrieves site information by its ID.
/// </summary>
/// <param name="SiteInfoId">The ID of the site information.</param>
/// <returns>The matching site information, or the first site information if not found.</returns>
public async Task<SiteInfo> GetSiteInfoByIdAsync(int SiteInfoId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Where(x => x.Id == SiteInfoId).FirstOrDefaultAsync();
if (result == null)
{
@ -703,17 +837,18 @@ namespace BLAIzor.Services
return result;
}
}
}
/// <summary>
/// Retrieves site information by its ID, including associated form definitions.
/// </summary>
/// <param name="siteId">The ID of the site.</param>
/// <returns>The matching site information with form definitions, or a new <see cref="SiteInfo"/> if not found.</returns>
public async Task<SiteInfo?> GetSiteInfoWithFormsByIdAsync(int siteId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Include(s => s.FormDefinitions).FirstOrDefaultAsync(s => s.Id == siteId);
if (result == null)
{
@ -724,16 +859,18 @@ namespace BLAIzor.Services
return result;
}
}
}
/// <summary>
/// Retrieves site information by its name.
/// </summary>
/// <param name="siteName">The name of the site.</param>
/// <returns>The matching site information, or the first site information if not found.</returns>
public async Task<SiteInfo> GetSiteInfoByNameAsync(string siteName)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Where(x => x.SiteName == siteName).FirstOrDefaultAsync();
if (result == null)
{
@ -743,18 +880,19 @@ namespace BLAIzor.Services
{
return result;
}
}
}
/// <summary>
/// Retrieves site information by its default URL.
/// </summary>
/// <param name="url">The default URL of the site.</param>
/// <returns>The matching site information, or the first site information if not found.</returns>
public async Task<SiteInfo> GetSiteInfoByUrlAsync(string url)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
var result = await _context.SiteInfos.Where(x => x.DefaultUrl == url).FirstOrDefaultAsync();
if (result == null)
{
@ -764,111 +902,71 @@ namespace BLAIzor.Services
{
return result;
}
}
}
/// <summary>
/// Updates existing site information in the database.
/// </summary>
/// <param name="siteInfo">The site information with updated details.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task UpdateSiteInfoAsync(SiteInfo siteInfo)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
_context.SiteInfos.Update(siteInfo);
await _context.SaveChangesAsync();
}
}
//collectionName change detection... do we really need to check? it should not be manually set.. if set, whole rechunk could be intitated...
//public async Task UpdateSiteInfoAndReChunkIfNeededAsync(SiteInfo updatedSiteInfo)
//{
// using var scope = _serviceScopeFactory.CreateScope();
// var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// var existing = await _context.SiteInfos
// .AsNoTracking()
// .FirstOrDefaultAsync(s => s.Id == updatedSiteInfo.Id);
// if (existing == null)
// throw new Exception("SiteInfo not found.");
// bool collectionChanged = existing.VectorCollectionName != updatedSiteInfo.VectorCollectionName;
// _context.SiteInfos.Update(updatedSiteInfo);
// await _context.SaveChangesAsync();
// if (collectionChanged)
// {
// // Create new collection if needed
// bool exists = await _qDrantService.CollectionExistsAsync(updatedSiteInfo.VectorCollectionName);
// if (!exists)
// {
// bool created = await _qDrantService.CreateQdrantCollectionAsync(updatedSiteInfo.VectorCollectionName);
// if (!created)
// throw new Exception("Failed to create new Qdrant collection.");
// }
// // Load all ContentItems for this site
// var contentItems = await _context.ContentItems
// .Include(ci => ci.ContentGroup)
// .Where(ci => ci.ContentGroup.SiteInfoId == updatedSiteInfo.Id)
// .ToListAsync();
// foreach (var item in contentItems)
// {
// // Rechunk into new collection
// await SaveAndSyncContentItemAsync(item, updatedSiteInfo.VectorCollectionName, true);
// }
// }
//}
/// <summary>
/// Retrieves all sites associated with a specific user.
/// </summary>
/// <param name="userId">The ID of the user.</param>
/// <returns>A list of sites belonging to the specified user.</returns>
public async Task<List<SiteInfo>> GetUserSitesAsync(string userId)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
return await _context.SiteInfos
.Where(s => s.UserId == userId)
.ToListAsync();
}
}
//public async Task<List<SiteInfo>> GetSitesAsync()
//{
// using (var scope = _serviceScopeFactory.CreateScope())
// {
// var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// // Now use dbContext safely without violating DI rules
// return await _context.SiteInfos.ToListAsync();
// }
//}
/// <summary>
/// Retrieves all site information records from the database.
/// </summary>
/// <returns>A list of all site information records.</returns>
public async Task<List<SiteInfo>> GetAllSitesAsync()
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
return await _context.SiteInfos.ToListAsync();
}
}
/// <summary>
/// Adds new site information to the database and creates a corresponding Qdrant collection.
/// </summary>
/// <param name="siteInfo">The site information to add.</param>
/// <returns>The added site information.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="siteInfo"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if an error occurs during the site creation or Qdrant collection creation.</exception>
public async Task<SiteInfo> AddSiteInfoAsync(SiteInfo siteInfo)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Now use dbContext safely without violating DI rules
if (siteInfo == null)
throw new ArgumentNullException(nameof(siteInfo), "SiteInfo cannot be null.");
try
{
if (string.IsNullOrEmpty(siteInfo.VectorCollectionName))
{
siteInfo.VectorCollectionName = GetGeneratedVectorCollectionName(siteInfo);
@ -878,14 +976,10 @@ namespace BLAIzor.Services
var result = await _context.SaveChangesAsync();
if (result > 0)
{
//check if collection exists already
bool checkResult = await _qDrantService.CollectionExistsAsync(siteInfo.VectorCollectionName);
if (checkResult)
{
//collection already exists (shouldn't exists, so it is occupied...), we need to create a new one.
//collection does not exist, create it
siteInfo.VectorCollectionName = GetGeneratedVectorCollectionName(siteInfo);
//update the site info with the new collection name
_context.SiteInfos.Update(siteInfo);
await _context.SaveChangesAsync();
bool qresult = await _qDrantService.CreateQdrantCollectionAsync(siteInfo.VectorCollectionName);
@ -896,7 +990,6 @@ namespace BLAIzor.Services
}
else
{
//collection does not exist, create it
bool qresult = await _qDrantService.CreateQdrantCollectionAsync(siteInfo.VectorCollectionName);
if (!qresult)
{
@ -908,14 +1001,14 @@ namespace BLAIzor.Services
}
catch (Exception ex)
{
// Log the exception (using a logging framework like Serilog, NLog, etc.)
throw new InvalidOperationException("An error occurred while adding the site info.", ex);
}
}
}
#endregion
///TEMPORARY
///
public async Task<bool> MigrateQdrantToContentItemsAsync(int siteId, string collectionName)
@ -925,12 +1018,12 @@ namespace BLAIzor.Services
var site = await GetSiteInfoByIdAsync(siteId);
// 1. Get menu items
var menuItems = await GetMenuItemsBySiteIdAsync(siteId);
PointId[] pointIds = new PointId[menuItems.Count];
for (int i=0; i < menuItems.Count; i++)
PointId[] pointIds = new PointId[menuItems.Count];
for (int i = 0; i < menuItems.Count; i++)
{
// Ensure SortOrder is set to i+1 (1-based index)
pointIds[i] = Convert.ToUInt64(menuItems[i].SortOrder);
}
}
if (!pointIds.Any())
return false;

213
Services/KimiApiService.cs Normal file
View File

@ -0,0 +1,213 @@
using BLAIzor.Models;
using Newtonsoft.Json;
using System.Net.Http;
using System.Text.Json;
using System.Text;
namespace BLAIzor.Services
{
public class KimiApiService
{
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
public KimiApiService(IConfiguration configuration, HttpClient httpClient)
{
_configuration = configuration;
_httpClient = httpClient;
}
private const string DeepSeekEndpoint = "https://api.moonshot.ai/v1/chat/completions";
public string _apiKey;
private Action<string> _callback;
public string GetApiKey()
{
if (_configuration == null)
{
return string.Empty;
}
if (_configuration.GetSection("Kimi") == null)
{
return string.Empty;
}
return _configuration.GetSection("Kimi").GetValue<string>("ApiKey")!;
}
public void RegisterCallback(Action<string> callback)
{
_callback = callback;
}
public async Task<string> GetSimpleChatGPTResponse(string systemMessage, string userMessage, string? assistantMessage = null)
{
_apiKey = GetApiKey();
var requestBody = new ChatGPTRequest();
if (assistantMessage == null)
{
requestBody = new ChatGPTRequest
{
Model = "moonshot-v1-8k",
Temperature = 0.2,
Messages = new[]
{
new Message { Role = "system", Content = systemMessage },
new Message { Role = "user", Content = userMessage }
},
Stream = false
};
}
else
{
requestBody = new ChatGPTRequest
{
Model = "moonshot-v1-8k",
Temperature = 0.2,
Messages = new[]
{
new Message { Role = "system", Content = systemMessage },
new Message {Role = "assistant", Content = assistantMessage },
new Message { Role = "user", Content = userMessage }
},
Stream = false
};
}
string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
}
);
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
var response = await _httpClient.PostAsync(DeepSeekEndpoint, requestContent);
response.EnsureSuccessStatusCode();
Console.Write(response.Content.ReadAsStringAsync());
var responseBody = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.Write(responseBody.GetRawText());
var result = responseBody.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString() ?? "No response";
Console.Write("Answer: " + result);
return result;
}
public async Task<string> GetChatGPTStreamedResponse(string sessionId, string systemMessage, string userMessage, string? assistanMessage = null)
{
_apiKey = GetApiKey();
ChatGPTRequest finalRequestBody;
if (assistanMessage == null)
{
finalRequestBody = new ChatGPTRequest
{
Model = "kimi-k2-0711-preview",
Temperature = 0.2,
Messages = new[]
{
new Message { Role = "system", Content = systemMessage },
new Message { Role = "user", Content = userMessage }
},
Stream = true
};
}
else
{
finalRequestBody = new ChatGPTRequest
{
Model = "kimi-k2-0711-preview",
Temperature = 0.2,
Messages = new[]
{
new Message { Role = "system", Content = systemMessage },
new Message {Role = "assistant", Content= assistanMessage},
new Message { Role = "user", Content = userMessage }
},
Stream = true
};
}
string requestJson = System.Text.Json.JsonSerializer.Serialize(finalRequestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
}
);
var finalRequestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
Console.Write(finalRequestContent);
var httpRequest = new HttpRequestMessage(HttpMethod.Post, DeepSeekEndpoint)
{
Content = finalRequestContent
};
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
// Use SendAsync with streamed response
var sResponse = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
sResponse.EnsureSuccessStatusCode();
using var responseStream = await sResponse.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);
//Console.Write("Streamed response:");
string streamedHtmlContent = string.Empty;
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
//Console.WriteLine($"Raw Stream Line: {line}"); // Log each line
if (!string.IsNullOrWhiteSpace(line) && line.StartsWith("data: "))
{
var jsonResponse = line.Substring(6); // Remove "data: " prefix
if (jsonResponse == "[DONE]")
{
Console.WriteLine("Stream ended.");
break; // End of stream
}
try
{
//Console.WriteLine($"JSON Response: {jsonResponse}"); // Debug JSON response
//TODO: do we really have to += the content, or should we just invoke with the delta?
var chunk = JsonConvert.DeserializeObject<StreamedResponse>(jsonResponse);
if (chunk?.Choices != null && chunk.Choices.Count > 0 && chunk.Choices[0].Delta?.Content != null)
{
streamedHtmlContent += chunk.Choices[0].Delta.Content; // Append the streamed content
if (!string.IsNullOrEmpty(streamedHtmlContent) && _callback != null)
{
_callback?.Invoke(streamedHtmlContent);
}
//Console.WriteLine($"Appended Text: {chunk.Choices[0].Delta.Content}"); // Debug appended text
}
else
{
Console.WriteLine("No content in this chunk.");
}
}
catch (JsonSerializationException ex)
{
Console.WriteLine($"Deserialization error: {ex.Message}");
}
}
}
Console.WriteLine("Final streamed content:");
Console.WriteLine(streamedHtmlContent);
return streamedHtmlContent;
}
}
}

View File

@ -57,7 +57,8 @@ namespace BLAIzor.Services
var requestBody = new ChatGPTRequest
{
Model = modelName,
Temperature = 0.2,
//Temperature = 0.2,
Temperature = 1,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
@ -115,7 +116,7 @@ namespace BLAIzor.Services
var requestBody = new ChatGPTRequest
{
Model = modelName,
Temperature = 0.2,
Temperature = 1,
Messages = assistantMessage == null || assistantMessage == string.Empty
? new[]
{
@ -170,7 +171,7 @@ namespace BLAIzor.Services
var requestBody = new ChatGPTRequest
{
Model = modelName,
Temperature = 0.2,
Temperature = 1,
Messages = assistantMessage == null
? new[]
{

View File

@ -0,0 +1,142 @@
using System.Text.Json;
namespace BLAIzor.Services
{
public class ReplicateService
{
private readonly HttpClient _http;
public ReplicateService(HttpClient http)
{
_http = http;
}
public async Task<string> GenerateImageAsync(string prompt, bool removeBackground)
{
return await GenerateImageAsync("https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions", prompt, removeBackground);
}
public async Task<string> GenerateLogoAsync(string prompt, bool removeBackground)
{
return await GenerateImageAsync("https://api.replicate.com/v1/models/google/imagen-4-fast/predictions", prompt, removeBackground);
}
public async Task<string> GenerateImageAsync(string apiUrl, string prompt, bool removeBackground)
{
var request = new
{
input = new { prompt = prompt, aspect_ratio = "1:1", output_format = "jpg" }
};
var createResponse = await _http.PostAsJsonAsync(apiUrl, request);
createResponse.EnsureSuccessStatusCode();
var createJson = await createResponse.Content.ReadFromJsonAsync<JsonElement>();
if (!createJson.TryGetProperty("id", out var idProp))
throw new Exception("Replicate response missing prediction ID.");
string predictionId = idProp.GetString();
string status = "";
JsonElement finalJson;
for (int attempt = 0; attempt < 30; attempt++)
{
var getResponse = await _http.GetAsync($"https://api.replicate.com/v1/predictions/{predictionId}");
getResponse.EnsureSuccessStatusCode();
finalJson = await getResponse.Content.ReadFromJsonAsync<JsonElement>();
status = finalJson.GetProperty("status").GetString();
if (status == "succeeded")
{
var output = finalJson.GetProperty("output");
if (output.ValueKind == JsonValueKind.String)
{
var imageUrl = output.GetString();
if (removeBackground)
{
return await RemoveBackgroundAsync(imageUrl);
}
else
{
return imageUrl;
}
}
else if (output.ValueKind == JsonValueKind.Array)
{
var length = output.GetArrayLength();
var imageUrl = output[0].ToString();
if (removeBackground)
{
return await RemoveBackgroundAsync(imageUrl);
}
else
{
return imageUrl;
}
}
return "Replicate response succeeded but no output image URL found.";
}
else if (status == "failed")
{
return "Replicate prediction failed.";
}
await Task.Delay(2500);
}
return "Timeout waiting for Replicate prediction to complete.";
}
public async Task<string> RemoveBackgroundAsync(string imageUrl)
{
var request = new
{
version = "a029dff38972b5fda4ec5d75d7d1cd25aeff621d2cf4946a41055d7db66b80bc",
input = new { image = imageUrl }
};
var createResponse = await _http.PostAsJsonAsync("https://api.replicate.com/v1/predictions", request);
createResponse.EnsureSuccessStatusCode();
var createJson = await createResponse.Content.ReadFromJsonAsync<JsonElement>();
if (!createJson.TryGetProperty("id", out var idProp))
throw new Exception("Replicate response missing prediction ID.");
string predictionId = idProp.GetString();
string status = "";
JsonElement finalJson;
for (int attempt = 0; attempt < 20; attempt++)
{
var getResponse = await _http.GetAsync($"https://api.replicate.com/v1/predictions/{predictionId}");
getResponse.EnsureSuccessStatusCode();
finalJson = await getResponse.Content.ReadFromJsonAsync<JsonElement>();
status = finalJson.GetProperty("status").GetString();
if (status == "succeeded")
{
var output = finalJson.GetProperty("output");
if (output.ValueKind == JsonValueKind.String)
return output.GetString();
return "Replicate response succeeded but no output image URL found.";
}
else if (status == "failed")
{
return "Replicate prediction failed.";
}
await Task.Delay(1500);
}
return "Timeout waiting for Replicate prediction to complete.";
}
}
}

View File

@ -12,6 +12,7 @@ namespace BLAIzor.Services
public ScopedContentService(ApplicationDbContext context, IServiceScopeFactory serviceScopeFactory)
{
//_context = context;
SessionId = Guid.NewGuid().ToString();
_serviceScopeFactory = serviceScopeFactory;
}
@ -45,11 +46,11 @@ namespace BLAIzor.Services
// set { }
//}
public string? WebsiteDefaultLanguage { get; set; }
public string SelectedLanguage { get; set; } = "English";
public string SessionId { get; set; }
public string SessionId { get; }
}
}

View File

@ -1,19 +1,20 @@
using BLAIzor.Data;
using BLAIzor.Models;
using Microsoft.EntityFrameworkCore;
using System;
namespace BLAIzor.Services
{
public class SimpleLogger : ISimpleLogger
{
private readonly ApplicationDbContext _dbContext;
private readonly IDbContextFactory<ApplicationDbContext> _dbFactory;
private readonly IWebHostEnvironment _env;
private LogLevel _currentLevel = LogLevel.Info;
private bool _consoleEnabled = true;
public SimpleLogger(ApplicationDbContext dbContext, IWebHostEnvironment env)
public SimpleLogger(IDbContextFactory<ApplicationDbContext> dbFactory, IWebHostEnvironment env)
{
_dbContext = dbContext;
_dbFactory = dbFactory;
_env = env;
}
@ -42,10 +43,20 @@ namespace BLAIzor.Services
Timestamp = DateTime.UtcNow
};
if (_env.IsProduction())
if (_env.IsDevelopment())
{
_dbContext.Logs.Add(log);
await _dbContext.SaveChangesAsync();
await using var db = await _dbFactory.CreateDbContextAsync();
log = new AppLog
{
Severity = level.ToString(),
Message = message,
Details = details,
Timestamp = DateTime.UtcNow
};
db.Logs.Add(log);
await db.SaveChangesAsync();
}
if (_consoleEnabled)
@ -53,7 +64,7 @@ namespace BLAIzor.Services
var color = Console.ForegroundColor;
Console.ForegroundColor = level switch
{
LogLevel.Info => ConsoleColor.Gray,
LogLevel.Info => ConsoleColor.Green,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Error => ConsoleColor.Red,
_ => ConsoleColor.White

View File

@ -0,0 +1,52 @@
namespace BLAIzor.Services
{
using System.Net.Http.Headers;
using System.Text.Json;
public class WhisperTranscriptionService
{
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
private string _apiKey = ""; // Store this securely!
public WhisperTranscriptionService(IHttpClientFactory httpClientFactory, IConfiguration configuration)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
}
private string GetApiKey() =>
_configuration?.GetSection("OpenAI")?.GetValue<string>("ApiKey") ?? string.Empty;
public async Task<string?> TranscribeAsync(byte[] audioData)
{
_apiKey = GetApiKey();
try
{
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
var content = new MultipartFormDataContent
{
{ new ByteArrayContent(audioData), "file", "audio.webm" },
{ new StringContent("whisper-1"), "model" }
};
var response = await client.PostAsync("https://api.openai.com/v1/audio/transcriptions", content);
response.EnsureSuccessStatusCode();
var resultJson = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(resultJson);
return json.RootElement.GetProperty("text").GetString();
}
catch (Exception ex)
{
Console.WriteLine("STT error: " + ex.Message);
return null;
}
}
}
}

8
_projectfiles/TODO.txt Normal file
View File

@ -0,0 +1,8 @@
Menuitem saving procedure fix
MenuItem reordering fix
Caching vector hash and version checking
Vector hashes?
AI LayoutBuilder sophistication
Language detection fix
Language detection for AI voice fix
TTS special characters localization (done)

View File

@ -6,7 +6,7 @@
}
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=194.164.235.47;Initial Catalog=BLAIzor;Integrated Security=False;Persist Security Info=False;User ID=sa;Password=v6f_?xNfg9N1;Trust Server Certificate=True"
"DefaultConnection": "Data Source=195.26.231.218;Initial Catalog=BLAIzor;Integrated Security=False;Persist Security Info=False;User ID=sa;Password=v6f_?xNfg9N1;Trust Server Certificate=True"
//"DefaultConnection": "Data Source=185.51.190.197;Initial Catalog=BLAIzor;Integrated Security=False;Persist Security Info=False;User ID=sa;Password=v6f_?xNfg9N1;Trust Server Certificate=True"
//"DefaultConnection": "Server=tcp:poppixel.database.windows.net,1433;Initial Catalog=Poppixel;Integrated Security=False;Persist Security Info=False;User ID=Adam;Password=v6f_?xNfg9N1;TrustServerCertificate=True;Connection Timeout=30"
},
@ -22,9 +22,10 @@
},
"AiSettings": {
"Provider": "cerebras",
//"Provider": "kimi",
//"Provider": "chatgpt",
"VoiceActivated": true,
"EmbeddingService": "openai"
"VoiceActivated": true,
"EmbeddingService": "openai"
//"EmbeddingService": "local"
},
"DeepSeek": {
@ -32,19 +33,24 @@
},
"Cerebras": {
"ApiKey": "csk-3pwm3pjjrpcmmt6rm6k8f43n6rhh3h5pjn6jn9m9j4pyevrp",
"Model": "llama-3.3-70b"
"Model": "gpt-oss-120b"
//"Model": "llama-3.3-70b"
//"Model": "llama-4-scout-17b-16e-instruct"
//"Model": "qwen-3-32b"
//"Model": "deepseek-r1-distill-llama-70b"
//"Model": "llama3.1-8b"
},
"Kimi": {
"ApiKey": "sk-GSHABNe1qCpfNmMTQfjtb57j1OOMyJcyMYJqVRV5EXZmcaBM"
},
"OpenAI": {
//"CredentialsPath": "D:\\GOOGLECREDENTIALS\\client_secret_359861037120-m3mjvr3kg51i2c2qb38dav62uuqoqs5k.apps.googleusercontent.com.json"
"ApiKey": "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",
//"ApiKey": "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" //VG3Law
//"Model": "gpt-4.1-mini"
"Model": "gpt-4o-mini"
//"Model": "gpt-4o-mini"
//"Model": "gpt-4.1-nano"
"Model": "gpt-5-nano"
},
"QDrant": {
//"CredentialsPath": "D:\\GOOGLECREDENTIALS\\client_secret_359861037120-m3mjvr3kg51i2c2qb38dav62uuqoqs5k.apps.googleusercontent.com.json"
@ -57,5 +63,9 @@
"ElevenLabsAPI": {
"ApiKey": "sk_adaa84dce6ed60504c71aff230f2b8bdbd0effa347f715b6"
},
"ScraperSettings": {
"Provider": "BrightData",
"ApiKey": "2137725d-f768-49fd-9c85-f9caf90518e7"
},
"AllowedHosts": "*"
}

View File

@ -1,6 +1,63 @@
/*@import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap');
@import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300');*/
article {
padding: 0px !important;
}
.admin-body {
background-color: #060816;
min-height: 100vh;
color: #fff;
--rz-base-800: #36244c !important;
--rz-base-background-color: #36244c !important;
}
.admin-body p {
font-size: 0.9rem;
}
.admin-body .btn {
font-size: 0.9rem;
}
.admin-body .btn:hover {
color: #000 !important;
background-color: aliceblue;
border-color: var(--bs-btn-hover-border-color) transparent;
}
.rz-panel {
background-color: transparent !important;
color: #fff !important;
--rz-text-h6-color: #fff !important;
--rz-text-body1-color: #fff !important;
}
.rz-card {
background: transparent !important;
border: unset !important;
color: #fff !important;
--rz-text-h6-color: #fff !important;
--rz-text-body1-color: #fff !important;
}
.admin-rz-card {
background: linear-gradient(to bottom, #63358d, #7d3d7b) !important;
border: unset !important;
border-radius: 20px !important;
}
.rz-text-h6 {
}
.rz-text-body {
}
.rz-sidebar {
background-color: #36244c !important;
}
label {
display: unset !important;
color: #fff !important;
@ -12,20 +69,20 @@ label {
scrollbar-color: #87b1d6 #ffffff00;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 16px;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 16px;
}
*::-webkit-scrollbar-track {
background: #ffffff;
}
*::-webkit-scrollbar-track {
background: #ffffff;
}
*::-webkit-scrollbar-thumb {
background-color: #3e9fa3;
border-radius: 10px;
border: 3px solid #ffffff;
}
*::-webkit-scrollbar-thumb {
background-color: #3e9fa3;
border-radius: 10px;
border: 3px solid #ffffff;
}
.reference-button {
@ -36,7 +93,7 @@ label {
border-radius: 20px;
padding: 16px;
width: 100%;
color: #e0e0f0;
color: #e0e0f0;
/*box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);*/
/*margin-bottom: 15px;*/
}
@ -69,7 +126,7 @@ label {
font-weight: bold;
width: 36px;
height: 36px;
border-radius: 50%;
border-radius: 50%;
flex-shrink: 0;
}
@ -85,6 +142,10 @@ label {
background: linear-gradient(to bottom, #533e7e, #3c3666) !important;
}
.bg.bg-panel-gradient:hover {
background: linear-gradient(to bottom, #63358d, #7d3d7b) !important;
}
.bg-panel-gradient-highlight {
background: linear-gradient(to bottom, #63358d, #7d3d7b) !important;
}
@ -97,8 +158,8 @@ label {
background-color: transparent !important;
}
.rz-dialog-wrapper{
z-index: 10008 !important;
.rz-dialog-wrapper {
z-index: 10008 !important;
border-radius: 20px;
}
@ -118,7 +179,6 @@ label {
box-shadow: 8px 6px 8px 2px rgba(0, 0, 0, 0.4);
--rz-dialog-title-background-color: transparent;
--rz-primary: #87b1d6;
}
.rz-dialog .rz-button {
@ -135,25 +195,25 @@ label {
color: #608AAD !important;
}
.bg-panel .list-group {
--bs-list-group-color: unset !important;
--bs-list-group-bg: unset !important;
}
.bg-panel .list-group-item {
background-color: unset !important;
border: unset !important;
}
.bg-panel .list-group-item .btn {
margin: 5px;
.bg-panel .list-group {
--bs-list-group-color: unset !important;
--bs-list-group-bg: unset !important;
}
.bg-panel .content-item-list {
max-height: 300px;
overflow-y: scroll;
.bg-panel .list-group-item {
background-color: unset !important;
border: unset !important;
}
.bg-panel .list-group-item .btn {
margin: 5px;
}
.bg-panel .content-item-list {
max-height: 300px;
overflow-y: scroll;
}
.rz-dialog-confirm-buttons .rz-base {
background-color: #87B1D6 !important;
}
@ -204,19 +264,68 @@ Don't show
backdrop-filter: blur(6px);
color: #b0daff;
z-index: 10008 !important;
top: 150px;
top: 200px;
border-radius: 20px;
min-width: 200px;
max-width: 200px;
height: 80vh;
height: fit-content;
margin-left: 10px;
padding: 10px;
box-shadow: 8px 6px 8px 2px rgba(0, 0, 0, 0.4);
--rz-primary: #87b1d6;
/*overflow-y: scroll;*/
}
.editor-window .btn {
margin: 5px;
border-radius: 20px;
background-color: var(--rz-primary);
color: var(--rz-on-primary);
}
.editor-window strong {
color: #63f0f9;
}
.editor-window span {
color: #fff;
}
.editor-window small {
color: #fff;
}
.editor-button .text-muted {
color: #b2b2b2 !important;
}
.top-panel-outer {
position: fixed;
z-index: 10010 !important;
top: 0px;
left: 0px;
width: 100%;
height: fit-content;
}
.top-panel {
position: relative;
background-color: #0c2533 !important;
/*background: linear-gradient(153deg,rgba(12, 37, 51, 0.83) 0%, rgba(87, 188, 199, 0.81) 50%, rgba(237, 83, 196, 0.84) 100%);*/
background: linear-gradient(307deg, rgba(12, 37, 51, 0.83) 7%, rgb(152 87 199 / 73%) 96%);
backdrop-filter: blur(6px);
color: #b0daff;
z-index: 10010 !important;
top: 0px;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
height: 70px;
min-width: 500px;
width: 800px;
margin-left: 10px;
box-shadow: 8px 6px 8px 2px rgba(0, 0, 0, 0.4);
--rz-primary: #87b1d6;
overflow-y: scroll;
}
.editor-window .btn {
margin: 5px;
font-size: 0.9rem;
}
.radzen-popup {
@ -236,9 +345,9 @@ Don't show
/*background-color: #b0daff;*/
}
.radzen-popup p {
color: #fff
}
.radzen-popup p {
color: #fff
}
.rz-panel {
@ -249,3 +358,123 @@ Don't show
border-radius: 0px;
}
.admin-accordion {
--rz-accordion-item-background-color: transparent;
--rz-accordion-item-color: #fff;
}
.rz-html-editor {
background-color: transparent !important;
border: unset !important;
}
.rz-html-editor:focus-within {
border: unset !important;
outline: unset !important;
}
.rz-html-editor-toolbar {
background-color: transparent !important;
}
.rz-html-editor-content {
background-color: transparent !important;
color: #fff;
}
.rz-textarea {
background-color: transparent !important;
color: #fff !important;
background-image: linear-gradient(#62448e 50%, #5a4083 50%);
background-size: 100% 3rem;
font-size: 0.8rem;
border: 1px solid #CCC;
width: 100%;
/* height: 400px; */
line-height: 1.5rem !important;
margin: 0 auto;
padding: 4px 8px !important;
background-attachment: local;
}
.rz-textarea:focus {
background-color: transparent !important;
}
.rz-html-editor-content {
color: #fff !important;
}
.rz-accordion > .rz-expander {
background-color: transparent !important;
}
.rz-accordion-content {
background-color: transparent !important;
}
.rz-accordion-header {
background-color: #36244c !important;
}
.rounded {
border-radius: 20px !important;
}
.step-circle {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #5c4f83;
color: white;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin: 0 auto;
transition: background-color 0.3s;
}
.step-circle.current {
background-color: #87b1d6;
color: black;
}
.step-circle.completed {
background: linear-gradient(to bottom, #63358d, #7d3d7b) !important;
}
.progress-line {
height: 3px;
background-color: #555;
margin-top: 8px;
width: 100%;
}
.form-control {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 20px;
}
.btn {
background: #87b1d6;
border: 0 !important;
color: #000000;
width: fit-content !important;
font-weight: bold !important;
transition: all 0.2s ease !important;
margin: 15px !important;
border-radius: 20px !important;
}
.btn:focus {
border-width: 0 !important;
font-weight: bold !important;
transition: all 0.2s ease !important;
}
.btn:focus-visible {
border-width: 0 !important;
font-weight: bold !important;
transition: all 0.2s ease !important;
}

View File

@ -12,9 +12,9 @@ a, .btn-link {
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
/*.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
}*/
.content {
padding-top: 1.1rem;

View File

@ -2,6 +2,16 @@
@import url('https://fonts.googleapis.com/css?family=Comfortaa:400,700,300');*/
.voicebutton {
position: relative;
border-radius: 50%;
padding: 0px;
width: 40px;
height: 40px;
margin: 0 auto !important;
display: none;
}
.radzen-popup {
display: none;
position: absolute;
@ -125,7 +135,7 @@
}
.searchBox:hover > .searchInput {
width: calc(100% - 60px);
width: calc(100% - 120px);
padding: 0 6px;
}
@ -134,6 +144,10 @@
color: #2f3640;
}
.searchBox:hover > .voicebutton {
display: unset;
}
.searchButton {
color: white;
float: right;
@ -309,9 +323,7 @@ ul {
list-style-type: none;
}
.rz-html-editor {
background-color: transparent !important;
}
h1 {

View File

@ -106,3 +106,13 @@ window.initVoiceRecorder = function (dotnetMethodName) {
}
};
};
window.seemgenAnimationHelper = {
restartAnimation: function (el) {
if (!el) return;
el.classList.remove("animate__animated");
void el.offsetWidth; // Reflow trick
el.classList.add("animate__animated");
}
};

View File

@ -0,0 +1,30 @@
let mediaRecorder;
let recordedChunks = [];
window.startRecording = async () => {
recordedChunks = [];
console.log("startRecording called");
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
const arrayBuffer = await blob.arrayBuffer();
const byteArray = new Uint8Array(arrayBuffer);
// Send to Blazor server
DotNet.invokeMethodAsync('BLAIzor', 'SendAudioToServer', Array.from(byteArray));
};
mediaRecorder.start();
};
window.stopRecording = () => {
mediaRecorder.stop();
};

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

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