VideoStudy/VideoStudy.UI/Pages/Home.razor

352 lines
14 KiB
Plaintext

@page "/"
@inject HttpClient Http
@inject IPdfSaver PdfSaver
@using VideoStudy.Shared
@using System.Net.Http.Json
@using System.Text.Json
@using System.Threading // Added for Cancellation
<PageTitle>VideoStudy - Video Analysis</PageTitle>
<div class="container-fluid py-4">
<!-- Header -->
<header class="text-center mb-5">
<h1 class="display-4 fw-bold" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
VideoStudy (Native)
</h1>
<p class="lead text-muted">Transforme videos em apostilas inteligentes com IA</p>
</header>
<div class="row">
<div class="col-lg-8 mx-auto">
<!-- Tabs Navigation -->
<ul class="nav nav-pills nav-fill mb-4 shadow-sm p-1 bg-light rounded-pill">
<li class="nav-item">
<button class="nav-link rounded-pill py-3 fw-bold @(activeTab == "youtube" ? "active bg-primary text-white" : "text-muted")"
@onclick="@(() => activeTab = "youtube")" disabled="@(isProcessing || isFetchingInfo)">
<span style="font-size:1.1rem;">YouTube Video</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link rounded-pill py-3 fw-bold text-muted" disabled>
Local Video (Coming Soon)
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="card shadow-soft border-0 rounded-4 overflow-hidden mb-4 p-4 bg-white">
@if (activeTab == "youtube")
{
<div class="mb-4">
<label class="form-label fw-bold text-muted small">YouTube URL</label>
<div class="input-group mb-3">
<input type="text" class="form-control form-control-lg" placeholder="https://www.youtube.com/watch?v=..."
@bind="videoUrl" disabled="@(isProcessing || isFetchingInfo)" />
</div>
</div>
}
<!-- Video Info Preview -->
@if (videoInfo != null)
{
<div class="border rounded-3 p-3 mb-4 d-flex align-items-start gap-3" style="background: #f0f4ff;">
@if (!string.IsNullOrEmpty(videoInfo.ThumbnailUrl))
{
<img src="@videoInfo.ThumbnailUrl" alt="Thumbnail"
style="width: 180px; border-radius: 8px; flex-shrink: 0;" />
}
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">@videoInfo.Title</h6>
<div class="text-muted small">
<span class="me-3">@videoInfo.Author</span>
<span>@videoInfo.Duration.ToString(@"hh\:mm\:ss")</span>
</div>
</div>
<button class="btn btn-sm btn-outline-secondary" @onclick="ClearVideoInfo"
disabled="@(isProcessing)" title="Limpar">X</button>
</div>
}
<!-- Language Selectors -->
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-bold text-muted small">Idioma do Video (Legendas)</label>
<select class="form-select" @bind="selectedLanguage" disabled="@(isProcessing || isFetchingInfo)">
<option value="en">English</option>
<option value="pt">Portuguese (BR)</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="ja">Japanese</option>
<option value="ko">Korean</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold text-muted small">Idioma de Saida (Tutorial)</label>
<select class="form-select" @bind="selectedOutputLanguage" disabled="@(isProcessing || isFetchingInfo)">
<option value="pt">Portuguese (BR)</option>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-4">
@if (videoInfo == null)
{
<!-- Step 1: Fetch video info -->
<button class="btn btn-lg btn-primary w-100 py-3 fw-bold shadow-sm"
@onclick="FetchVideoInfo"
disabled="@(isProcessing || isFetchingInfo || string.IsNullOrWhiteSpace(videoUrl))">
@if (isFetchingInfo)
{
<span class="spinner-border spinner-border-sm me-2"></span>
<span>Verificando video...</span>
}
else
{
<span>Generate Tutorial PDF</span>
}
</button>
}
else
{
<!-- Step 2: Confirm and generate -->
<div class="d-flex gap-2">
<button class="btn btn-lg btn-success flex-grow-1 py-3 fw-bold shadow-sm"
@onclick="StartAnalysis"
disabled="@isProcessing">
@if (isProcessing)
{
<span class="spinner-border spinner-border-sm me-2"></span>
<span>Gerando PDF...</span>
}
else
{
<span>Confirmar e Gerar PDF</span>
}
</button>
</div>
}
</div>
</div>
<!-- Progress Indicator -->
@if (isProcessing || currentStep > 0)
{
<div class="progress mb-3" style="height: 25px;">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: @(progress)%">
@statusMessage
</div>
</div>
}
<!-- Logs (Collapsible) -->
@if (logs.Count > 0)
{
<div class="card shadow-sm mb-4 border-0 rounded-4 overflow-hidden">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" style="cursor: pointer;" @onclick="ToggleLogs">
<small class="fw-bold">Execution Logs</small>
<small>@logs.Count items @(showLogs ? "v" : ">")</small>
</div>
@if (showLogs)
{
<div class="card-body bg-light" style="max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
@foreach (var log in logs)
{
<div class="text-dark border-bottom py-1">
<span class="text-muted">[@log.Timestamp:HH:mm:ss]</span> @log.Message
</div>
}
</div>
}
</div>
}
</div>
</div>
</div>
<style>
.nav-pills .nav-link.active {
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
}
.shadow-soft {
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
</style>
@code {
private string activeTab = "youtube";
private bool isProcessing = false;
private bool isFetchingInfo = false;
private int progress = 0;
private int currentStep = 0;
private string statusMessage = "Ready";
private bool showLogs = false;
private List<LogEntry> logs = new();
private string videoUrl = string.Empty;
private string selectedLanguage = "en";
private string selectedOutputLanguage = "pt";
private VideoInfo? videoInfo;
private class LogEntry
{
public DateTime Timestamp { get; set; } = DateTime.Now;
public string Message { get; set; } = string.Empty;
}
private void ToggleLogs() => showLogs = !showLogs;
private void AddLog(string message, int? eventProgress = null)
{
logs.Insert(0, new LogEntry { Message = message });
if (eventProgress.HasValue) progress = eventProgress.Value;
StateHasChanged();
}
private void ClearVideoInfo()
{
videoInfo = null;
}
private async Task FetchVideoInfo()
{
if (string.IsNullOrWhiteSpace(videoUrl)) return;
isFetchingInfo = true;
showLogs = true;
try
{
AddLog("Buscando informacoes do video...");
var response = await Http.GetAsync($"api/video-info?url={Uri.EscapeDataString(videoUrl)}");
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new Exception($"API Error: {error}");
}
videoInfo = await response.Content.ReadFromJsonAsync<VideoInfo>(
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (videoInfo != null)
AddLog($"Video encontrado: {videoInfo.Title} ({videoInfo.Duration:hh\\:mm\\:ss})");
}
catch (Exception ex)
{
AddLog($"Erro: {ex.Message}");
}
finally
{
isFetchingInfo = false;
}
}
private async Task StartAnalysis()
{
isProcessing = true;
progress = 0;
currentStep = 0;
logs.Clear();
showLogs = true;
CancellationTokenSource? cts = null;
try
{
currentStep = 1;
statusMessage = "Iniciando análise...";
AddLog("🚀 Enviando requisição para a API...", 5);
var request = new AnalysisRequest
{
VideoUrl = videoUrl,
Language = selectedLanguage,
OutputLanguage = selectedOutputLanguage,
Mode = "native"
};
cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "api/analyze")
{
Content = JsonContent.Create(request)
};
using var response = await Http.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new Exception($"API Error ({response.StatusCode}): {error}");
}
var stream = response.Content.ReadFromJsonAsAsyncEnumerable<AnalysisEvent>(new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, cancellationToken);
await foreach (var analysisEvent in stream.WithCancellation(cancellationToken))
{
if (analysisEvent == null) continue;
if (analysisEvent.IsError)
{
throw new Exception($"Erro da API: {analysisEvent.Message}");
}
AddLog(analysisEvent.Message, analysisEvent.ProgressPercentage);
statusMessage = analysisEvent.Message;
progress = analysisEvent.ProgressPercentage;
StateHasChanged();
if (analysisEvent.Result != null)
{
var result = analysisEvent.Result;
AddLog("✅ Análise concluída!", 100);
statusMessage = "Pronto!";
if (result.PdfData != null && result.PdfData.Length > 0)
{
AddLog("Salvando PDF...");
var fileName = $"VideoStudy_{result.VideoTitle ?? "Tutorial"}_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
// Remove invalid filename chars
fileName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
var savedPath = await PdfSaver.SavePdfAsync(result.PdfData, fileName);
if (savedPath != null)
AddLog($"✓ PDF salvo: {savedPath}");
else
AddLog("⚠️ Salvamento do PDF cancelado pelo usuário");
}
else
{
AddLog("⚠️ Nenhum dado de PDF retornado da API");
}
break;
}
}
}
catch (OperationCanceledException)
{
AddLog("⏸️ Operação cancelada.");
statusMessage = "Cancelado";
}
catch (Exception ex)
{
AddLog($"❌ Erro: {ex.Message}");
statusMessage = "Erro";
}
finally
{
isProcessing = false;
videoInfo = null;
cts?.Dispose();
}
}
}