SeemGen/Components/Partials/GenerateSitePages.razor

512 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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";
}
}