SeemGen/Components/Partials/CreateSiteWizard.razor

502 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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();
}
}
}