328 lines
13 KiB
Plaintext
328 lines
13 KiB
Plaintext
@page "/"
|
|
@inject HttpClient Http
|
|
@inject IPdfSaver PdfSaver
|
|
@using VideoStudy.Shared
|
|
@using System.Net.Http.Json
|
|
@using System.Text.Json
|
|
|
|
<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)
|
|
{
|
|
logs.Insert(0, new LogEntry { Message = message });
|
|
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;
|
|
|
|
try
|
|
{
|
|
currentStep = 1;
|
|
statusMessage = "Calling API...";
|
|
AddLog("Sending request to API...");
|
|
|
|
var request = new AnalysisRequest
|
|
{
|
|
VideoUrl = videoUrl,
|
|
Language = selectedLanguage,
|
|
OutputLanguage = selectedOutputLanguage,
|
|
Mode = "native"
|
|
};
|
|
|
|
// Long timeout via CancellationToken (HttpClient.Timeout can't be changed after first request)
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
|
|
|
|
var response = await Http.PostAsJsonAsync("api/analyze", request, cts.Token);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = await response.Content.ReadAsStringAsync();
|
|
throw new Exception($"API Error ({response.StatusCode}): {error}");
|
|
}
|
|
|
|
var json = await response.Content.ReadAsStringAsync();
|
|
var result = JsonSerializer.Deserialize<AnalysisResponse>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
|
|
if (result == null) throw new Exception("Failed to deserialize response");
|
|
|
|
if (result.DebugSteps != null)
|
|
{
|
|
foreach (var step in result.DebugSteps) AddLog($"[API] {step}");
|
|
}
|
|
|
|
if (result.Status == "error") throw new Exception(result.ErrorMessage);
|
|
|
|
AddLog("Analysis Complete!");
|
|
progress = 100;
|
|
statusMessage = "Done!";
|
|
|
|
if (result.PdfData != null && result.PdfData.Length > 0)
|
|
{
|
|
AddLog("Saving 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 saved: {savedPath}");
|
|
else
|
|
AddLog("PDF save cancelled by user");
|
|
}
|
|
else
|
|
{
|
|
AddLog("No PDF data returned from API");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AddLog($"Error: {ex.Message}");
|
|
statusMessage = "Error";
|
|
}
|
|
finally
|
|
{
|
|
isProcessing = false;
|
|
videoInfo = null;
|
|
}
|
|
}
|
|
}
|