summarizer/ConsoleRenderer.cs

173 lines
7.2 KiB
C#
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 Spectre.Console;
using YoutubeSummarizer.Models;
namespace YoutubeSummarizer.Services;
/// <summary>
/// Handles all console output formatting.
/// Keeping display logic separate from business logic makes it easy to
/// later add output modes (JSON, Markdown file, HTML report) without
/// touching the service layer.
///
/// Rendering is delegated to Spectre.Console, which handles colour,
/// word-wrap to the current terminal width, and graceful degradation
/// when output is redirected to a file or a non-ANSI terminal.
/// </summary>
public static class ConsoleRenderer
{
/// <summary>Prints the application banner on startup.</summary>
public static void PrintBanner()
{
AnsiConsole.WriteLine();
AnsiConsole.Write(new FigletText("YT Summary").Centered().Color(Color.Red));
AnsiConsole.Write(
new Rule("[grey]YouTube Video Summarizer[/]")
.RuleStyle("grey")
.Centered());
AnsiConsole.WriteLine();
}
/// <summary>
/// True when the terminal can drive Spectre's interactive prompts. When output
/// is redirected or piped we fall back to plain reads / default answers so the
/// app still works in scripts and CI.
/// </summary>
private static bool Interactive => AnsiConsole.Profile.Capabilities.Interactive;
/// <summary>Prompts the user for a URL and reads input.</summary>
public static string PromptForUrl()
{
if (!Interactive)
{
AnsiConsole.Markup("[bold]Enter YouTube URL[/] [grey](or 'q' to quit)[/]: ");
return Console.ReadLine()?.Trim() ?? string.Empty;
}
var input = AnsiConsole.Prompt(
new TextPrompt<string>("[bold]Enter YouTube URL[/] [grey](or 'q' to quit)[/]:")
.AllowEmpty());
return input.Trim();
}
/// <summary>
/// Asks the user whether they want to save the transcript to a text file.
/// Defaults to <c>true</c> — pressing Enter accepts.
/// </summary>
public static bool PromptSaveTranscript()
{
if (!Interactive) return true;
return AnsiConsole.Confirm("[bold]Save transcript to file?[/]", defaultValue: true);
}
/// <summary>Prints a success message with the saved file path.</summary>
public static void PrintFileSaved(string filePath)
{
AnsiConsole.MarkupLine($"[green]✓[/] Transcript saved to: [blue]{Markup.Escape(filePath)}[/]");
AnsiConsole.WriteLine();
}
/// <summary>
/// Prompts the user to choose a summary mode via an arrow-key selection list.
/// <see cref="SummaryMode.Standard"/> is listed first so it is the highlighted
/// default — pressing Enter accepts it.
/// </summary>
public static SummaryMode PromptSummaryMode()
{
if (!Interactive) return SummaryMode.Standard;
return AnsiConsole.Prompt(
new SelectionPrompt<SummaryMode>()
.Title("[bold]Choose summary mode:[/]")
.AddChoices(SummaryMode.Standard, SummaryMode.PersonalFilter)
.UseConverter(m => m switch
{
SummaryMode.Standard => "Standard detailed bullet-point summary",
SummaryMode.PersonalFilter => "Personal Filter relevance verdict (ACT / MONITOR / IGNORE)",
_ => m.ToString()
}));
}
/// <summary>Prints a dim "working" indicator before an async step.</summary>
public static void PrintWorking(string message)
{
AnsiConsole.MarkupLine($"[grey]→ {Markup.Escape(message)}...[/]");
}
/// <summary>
/// Renders the full summary result to the console in a structured,
/// readable format. Includes metadata header, quality warning, and
/// the summary body.
/// </summary>
public static void PrintSummary(VideoSummary summary, bool showTranscriptSource)
{
AnsiConsole.WriteLine();
// ── Metadata header ──────────────────────────────────────────────────
var grid = new Grid();
grid.AddColumn(new GridColumn().PadRight(2));
grid.AddColumn();
grid.AddRow("[bold green]Title[/]", $"[bold]{Markup.Escape(summary.Metadata.Title)}[/]");
grid.AddRow("[grey]Channel[/]", Markup.Escape(summary.Metadata.ChannelTitle));
grid.AddRow("[grey]Published[/]", Markup.Escape(summary.Metadata.PublishedAt.ToString("MMMM d, yyyy")));
grid.AddRow("[grey]Duration[/]", Markup.Escape(summary.Metadata.FormattedDuration));
grid.AddRow("[grey]URL[/]", $"https://youtu.be/{Markup.Escape(summary.Metadata.VideoId)}");
if (showTranscriptSource)
{
var (badge, color) = summary.TranscriptSource switch
{
TranscriptSource.OwnerPublished => ("✓ Owner-published captions", "green"),
TranscriptSource.CommunityContributed => ("✓ Community captions", "green"),
TranscriptSource.AutoGenerated => ("~ Auto-generated (ASR)", "yellow"),
TranscriptSource.MetadataOnly => ("✗ Metadata only", "red"),
_ => ("? Unknown", "grey")
};
grid.AddRow("[grey]Transcript[/]", $"[{color}]{Markup.Escape(badge)}[/]");
}
grid.AddRow("[grey]Model[/]", Markup.Escape(summary.ModelUsed));
grid.AddRow("[grey]Generated[/]", Markup.Escape(summary.GeneratedAt.ToString("yyyy-MM-dd HH:mm")) + " UTC");
AnsiConsole.Write(grid);
AnsiConsole.WriteLine();
// ── Quality warning ──────────────────────────────────────────────────
if (summary.QualityWarning is not null)
{
AnsiConsole.Write(
new Panel($"[yellow]{Markup.Escape(summary.QualityWarning)}[/]")
{
Header = new PanelHeader("Quality warning"),
Border = BoxBorder.Rounded,
BorderStyle = new Style(Color.Yellow)
});
AnsiConsole.WriteLine();
}
// ── Summary body ─────────────────────────────────────────────────────
AnsiConsole.Write(
new Panel(Markup.Escape(summary.SummaryText))
{
Header = new PanelHeader("Summary"),
Border = BoxBorder.Rounded,
Padding = new Padding(2, 1, 2, 1)
}.Expand());
AnsiConsole.WriteLine();
}
/// <summary>Prints a styled error message.</summary>
public static void PrintError(string message)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[red]✗ Error:[/] {Markup.Escape(message)}");
AnsiConsole.WriteLine();
}
/// <summary>Prints a styled warning (non-fatal).</summary>
public static void PrintWarning(string message)
{
AnsiConsole.MarkupLine($"[yellow]⚠ {Markup.Escape(message)}[/]");
}
}