using Spectre.Console;
using YoutubeSummarizer.Models;
namespace YoutubeSummarizer.Services;
///
/// 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.
///
public static class ConsoleRenderer
{
/// Prints the application banner on startup.
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();
}
///
/// 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.
///
private static bool Interactive => AnsiConsole.Profile.Capabilities.Interactive;
/// Prompts the user for a URL and reads input.
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("[bold]Enter YouTube URL[/] [grey](or 'q' to quit)[/]:")
.AllowEmpty());
return input.Trim();
}
///
/// Asks the user whether they want to save the transcript to a text file.
/// Defaults to true — pressing Enter accepts.
///
public static bool PromptSaveTranscript()
{
if (!Interactive) return true;
return AnsiConsole.Confirm("[bold]Save transcript to file?[/]", defaultValue: true);
}
/// Prints a success message with the saved file path.
public static void PrintFileSaved(string filePath)
{
AnsiConsole.MarkupLine($"[green]✓[/] Transcript saved to: [blue]{Markup.Escape(filePath)}[/]");
AnsiConsole.WriteLine();
}
///
/// Prompts the user to choose a summary mode via an arrow-key selection list.
/// is listed first so it is the highlighted
/// default — pressing Enter accepts it.
///
public static SummaryMode PromptSummaryMode()
{
if (!Interactive) return SummaryMode.Standard;
return AnsiConsole.Prompt(
new SelectionPrompt()
.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()
}));
}
/// Prints a dim "working" indicator before an async step.
public static void PrintWorking(string message)
{
AnsiConsole.MarkupLine($"[grey]→ {Markup.Escape(message)}...[/]");
}
///
/// Renders the full summary result to the console in a structured,
/// readable format. Includes metadata header, quality warning, and
/// the summary body.
///
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();
}
/// Prints a styled error message.
public static void PrintError(string message)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[red]✗ Error:[/] {Markup.Escape(message)}");
AnsiConsole.WriteLine();
}
/// Prints a styled warning (non-fatal).
public static void PrintWarning(string message)
{
AnsiConsole.MarkupLine($"[yellow]⚠ {Markup.Escape(message)}[/]");
}
}