236 lines
8.7 KiB
Plaintext
236 lines
8.7 KiB
Plaintext
@page "/"
|
|
@inject HttpClient Http
|
|
@inject YouTubeService YouTubeService
|
|
@inject PersistenceService PersistenceService
|
|
@inject NavigationManager NavigationManager
|
|
@using VideoStudy.Shared
|
|
@using System.Net.Http.Json
|
|
@using System.Text.Json
|
|
@using System.Threading
|
|
|
|
<PageTitle>VideoStudy</PageTitle>
|
|
|
|
<div class="container-fluid py-4">
|
|
<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
|
|
</h1>
|
|
<p class="lead text-muted">Transforme vídeos em guias de estudo com IA</p>
|
|
</header>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8 mx-auto">
|
|
|
|
<div class="card shadow-sm border-0 rounded-4 overflow-hidden mb-4 p-4 bg-white">
|
|
<YouTubeProcessor
|
|
OnVideoUrlChanged="HandleVideoUrlChanged"
|
|
IsProcessing="@isProcessing"
|
|
VideoInfo="@currentVideoInfo" />
|
|
|
|
<div class="mt-3">
|
|
<label class="form-label fw-bold text-muted small">
|
|
Contexto <span class="fw-normal">(opcional)</span>
|
|
</label>
|
|
<textarea class="form-control" rows="2"
|
|
placeholder="Ex: tutorial técnico de Python para iniciantes / aula escolar sobre fotossíntese / passo a passo para montar um móvel"
|
|
@bind="userContext" disabled="@isProcessing"></textarea>
|
|
</div>
|
|
|
|
<div class="row g-3 mt-2">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-bold text-muted small">Idioma de saída</label>
|
|
<select class="form-select" @bind="selectedLanguage" disabled="@isProcessing">
|
|
<option value="pt">Português (BR)</option>
|
|
<option value="en">English</option>
|
|
<option value="es">Español</option>
|
|
<option value="fr">Français</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-grid mt-4">
|
|
<button class="btn btn-lg btn-primary" @onclick="StartAnalysis"
|
|
disabled="@(isProcessing || string.IsNullOrWhiteSpace(videoUrl))">
|
|
@if (isProcessing)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
<span>Analisando...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Analisar com IA</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (isProcessing || currentStep > 0)
|
|
{
|
|
<ProgressIndicator Status="@statusMessage" Percent="@progress" CurrentStepIndex="@currentStep" />
|
|
}
|
|
|
|
@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">Log de execução</small>
|
|
<small>@logs.Count eventos @(showLogs ? "▼" : "▶")</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.ToString("HH:mm:ss")]</span> @log.Message
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrEmpty(generatedPdfPath))
|
|
{
|
|
<PdfPreview PdfPath="@generatedPdfPath" OnCancel="@(() => generatedPdfPath = null)" />
|
|
}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private bool isProcessing = false;
|
|
private int progress = 0;
|
|
private int currentStep = 0;
|
|
private string statusMessage = "Pronto";
|
|
private bool showLogs = false;
|
|
private List<LogEntry> logs = new();
|
|
|
|
private string videoUrl = string.Empty;
|
|
private string selectedLanguage = "pt";
|
|
private string? userContext;
|
|
private VideoInfo? currentVideoInfo;
|
|
private string? generatedPdfPath;
|
|
|
|
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 (logs.Count > 100) logs.RemoveAt(logs.Count - 1);
|
|
if (eventProgress.HasValue) progress = eventProgress.Value;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void HandleVideoUrlChanged(string url)
|
|
{
|
|
videoUrl = url;
|
|
currentVideoInfo = null;
|
|
}
|
|
|
|
private async Task StartAnalysis()
|
|
{
|
|
isProcessing = true;
|
|
progress = 0;
|
|
currentStep = 0;
|
|
generatedPdfPath = null;
|
|
logs.Clear();
|
|
showLogs = true;
|
|
|
|
CancellationTokenSource? cts = null;
|
|
|
|
try
|
|
{
|
|
cts = new CancellationTokenSource();
|
|
var token = cts.Token;
|
|
|
|
// Buscar info do vídeo antes de iniciar
|
|
currentStep = 1;
|
|
statusMessage = "Buscando informações do vídeo...";
|
|
AddLog($"Verificando URL: {videoUrl}", 2);
|
|
try
|
|
{
|
|
currentVideoInfo = await YouTubeService.GetVideoInfoAsync(videoUrl);
|
|
AddLog($"Vídeo encontrado: {currentVideoInfo.Title}", 5);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new Exception($"Não foi possível obter informações do vídeo: {ex.Message}");
|
|
}
|
|
|
|
statusMessage = "Iniciando análise...";
|
|
AddLog("Enviando para o servidor...", 8);
|
|
|
|
var request = new AnalysisRequest
|
|
{
|
|
VideoUrl = videoUrl,
|
|
Mode = "fast",
|
|
Language = "en",
|
|
OutputLanguage = selectedLanguage,
|
|
DurationSeconds = currentVideoInfo?.Duration.TotalSeconds ?? 0,
|
|
UserContext = string.IsNullOrWhiteSpace(userContext) ? null : userContext.Trim()
|
|
};
|
|
|
|
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "api/analyze")
|
|
{
|
|
Content = JsonContent.Create(request)
|
|
};
|
|
|
|
using var response = await Http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, token);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var stream = response.Content.ReadFromJsonAsAsyncEnumerable<AnalysisEvent>(
|
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, token);
|
|
|
|
await foreach (var ev in stream.WithCancellation(token))
|
|
{
|
|
if (ev == null) continue;
|
|
if (ev.IsError) throw new Exception(ev.Message);
|
|
|
|
AddLog(ev.Message, ev.ProgressPercentage);
|
|
statusMessage = ev.Message;
|
|
StateHasChanged();
|
|
|
|
if (ev.Result != null)
|
|
{
|
|
AddLog("Análise concluída!", 100);
|
|
|
|
if (ev.Result.PdfData != null && ev.Result.PdfData.Length > 0)
|
|
{
|
|
AddLog("Salvando PDF na biblioteca...", 100);
|
|
var session = await PersistenceService.SaveSessionAsync(
|
|
ev.Result.PdfData,
|
|
ev.Result.DocumentTitle,
|
|
videoUrl);
|
|
|
|
generatedPdfPath = session.FilePath;
|
|
AddLog($"PDF salvo em: {session.FilePath}", 100);
|
|
|
|
await Task.Delay(2000);
|
|
NavigationManager.NavigateTo("library");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AddLog($"Erro: {ex.Message}");
|
|
statusMessage = "Erro na análise";
|
|
}
|
|
finally
|
|
{
|
|
isProcessing = false;
|
|
cts?.Dispose();
|
|
}
|
|
}
|
|
}
|