512 lines
20 KiB
Plaintext
512 lines
20 KiB
Plaintext
@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";
|
||
}
|
||
|
||
}
|