502 lines
20 KiB
Plaintext
502 lines
20 KiB
Plaintext
@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>18–25</option>
|
||
<option>26–40</option>
|
||
<option>41–60</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();
|
||
}
|
||
}
|
||
}
|