summarizer/ConsoleRenderer.cs

198 lines
8.5 KiB
C#
Raw Permalink 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 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.
/// </summary>
public static class ConsoleRenderer
{
// ANSI color codes. These render correctly in most Linux terminals.
// If you pipe output to a file, the escape codes will appear as-is —
// run with --no-color if that's a concern (not implemented here, left
// as an exercise).
private const string Reset = "\x1b[0m";
private const string Bold = "\x1b[1m";
private const string Cyan = "\x1b[36m";
private const string Yellow = "\x1b[33m";
private const string Green = "\x1b[32m";
private const string Red = "\x1b[31m";
private const string Dim = "\x1b[2m";
/// <summary>Prints the application banner on startup.</summary>
public static void PrintBanner()
{
Console.WriteLine();
Console.WriteLine($"{Bold}{Cyan}╔════════════════════════════════════════╗{Reset}");
Console.WriteLine($"{Bold}{Cyan}║ YouTube Video Summarizer ║{Reset}");
Console.WriteLine($"{Bold}{Cyan}╚════════════════════════════════════════╝{Reset}");
Console.WriteLine();
}
/// <summary>Prompts the user for a URL and reads input.</summary>
public static string PromptForUrl()
{
Console.Write($"{Bold}Enter YouTube URL (or 'q' to quit):{Reset} ");
return Console.ReadLine()?.Trim() ?? string.Empty;
}
/// <summary>
/// Asks the user whether they want to save the transcript to a text file.
/// Returns true if the user answers yes.
/// </summary>
public static bool PromptSaveTranscript()
{
Console.Write($"{Bold}Save transcript to file? (y/n):{Reset} ");
var answer = Console.ReadLine()?.Trim() ?? string.Empty;
return answer.Equals("y", StringComparison.OrdinalIgnoreCase)
|| answer.Equals("yes", StringComparison.OrdinalIgnoreCase);
}
/// <summary>Prints a success message with the saved file path.</summary>
public static void PrintFileSaved(string filePath)
{
Console.WriteLine($" {Green}✓ Transcript saved to:{Reset} {filePath}");
Console.WriteLine();
}
/// <summary>
/// Prompts the user to choose a summary mode.
/// Returns the selected <see cref="SummaryMode"/>.
/// </summary>
public static SummaryMode PromptSummaryMode()
{
Console.WriteLine($" {Dim}Summary modes:{Reset}");
Console.WriteLine($" {Bold}1{Reset} Standard (detailed bullet-point summary)");
Console.WriteLine($" {Bold}2{Reset} Personal Filter (relevance verdict: ACT / MONITOR / IGNORE)");
Console.Write($"{Bold}Choose summary mode [1]:{Reset} ");
var choice = Console.ReadLine()?.Trim() ?? string.Empty;
return choice == "2" ? SummaryMode.PersonalFilter : SummaryMode.Standard;
}
/// <summary>Displays a spinner-style "working" indicator while async work runs.</summary>
public static void PrintWorking(string message)
{
Console.WriteLine($" {Dim}→ {message}...{Reset}");
}
/// <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)
{
Console.WriteLine();
PrintDivider();
// ── Metadata header ──────────────────────────────────────────────────
Console.WriteLine($"{Bold}{Green} {summary.Metadata.Title}{Reset}");
Console.WriteLine($" {Dim}Channel:{Reset} {summary.Metadata.ChannelTitle}");
Console.WriteLine($" {Dim}Published:{Reset} {summary.Metadata.PublishedAt:MMMM d, yyyy}");
Console.WriteLine($" {Dim}Duration:{Reset} {summary.Metadata.FormattedDuration}");
Console.WriteLine($" {Dim}URL:{Reset} https://youtu.be/{summary.Metadata.VideoId}");
// ── Transcript source badge ──────────────────────────────────────────
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", Dim)
};
Console.WriteLine($" {Dim}Transcript:{Reset} {color}{badge}{Reset}");
}
Console.WriteLine($" {Dim}Model:{Reset} {summary.ModelUsed}");
Console.WriteLine($" {Dim}Generated:{Reset} {summary.GeneratedAt:yyyy-MM-dd HH:mm} UTC");
PrintDivider();
// ── Quality warning ──────────────────────────────────────────────────
if (summary.QualityWarning is not null)
{
Console.WriteLine();
Console.WriteLine($" {Yellow}{summary.QualityWarning}{Reset}");
}
// ── Summary body ─────────────────────────────────────────────────────
Console.WriteLine();
Console.WriteLine($"{Bold} SUMMARY{Reset}");
Console.WriteLine();
// Word-wrap the summary body at 80 characters so it's readable in
// standard terminal widths without horizontal scrolling.
foreach (var line in WordWrap(summary.SummaryText, maxWidth: 78))
{
Console.WriteLine($" {line}");
}
Console.WriteLine();
PrintDivider();
Console.WriteLine();
}
/// <summary>Prints a styled error message.</summary>
public static void PrintError(string message)
{
Console.WriteLine();
Console.WriteLine($" {Red}✗ Error: {message}{Reset}");
Console.WriteLine();
}
/// <summary>Prints a styled warning (non-fatal).</summary>
public static void PrintWarning(string message)
{
Console.WriteLine($" {Yellow}⚠ {message}{Reset}");
}
// ─────────────────────────────────────────────────────────────────────────
// Private helpers
// ─────────────────────────────────────────────────────────────────────────
private static void PrintDivider()
{
Console.WriteLine($" {Dim}{"".PadRight(74, '─')}{Reset}");
}
/// <summary>
/// Splits text into lines no wider than <paramref name="maxWidth"/> characters,
/// breaking only at word boundaries. Respects existing newlines in the input.
/// </summary>
private static IEnumerable<string> WordWrap(string text, int maxWidth)
{
foreach (var paragraph in text.Split('\n'))
{
if (string.IsNullOrWhiteSpace(paragraph))
{
yield return string.Empty;
continue;
}
var words = paragraph.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var current = new System.Text.StringBuilder();
foreach (var word in words)
{
if (current.Length + word.Length + 1 > maxWidth)
{
yield return current.ToString();
current.Clear();
}
if (current.Length > 0) current.Append(' ');
current.Append(word);
}
if (current.Length > 0)
yield return current.ToString();
}
}
}