RazorConsole로 요즈음 유행하는 CLI 느낌이 나는 콘솔 만들기

@BigSquare 님께서 NixOS에서 rust tauri + C# blazor wasm 를 위한 설정 글에서 댓글 달아주신 GitHub - LittleLittleCloud/RazorConsole: Build interactive console applications with .NET Razor and Spectre.Console 프로젝트의 샘플을 보고, FBA 버전으로 만들어본다면 재미있을 것 같아서 바로 만들어봤습니다. :smiley:

Components/App.razor

@namespace LLMAgentTUI.Components

@using System
@using System.Collections.Generic
@using System.Linq
@using Microsoft.AspNetCore.Components
@using RazorConsole.Components
@using Spectre.Console

@inject ChatService ChatService

<Figlet Content="ChatBot" />

<Align Horizontal="@HorizontalAlignment.Center">
	<Markup Content="AI-Powered Console ChatBot • Tab to change focus • Enter to submit • Ctrl+C to exit" Foreground="@Color.Grey58" />
</Align>
<Padder Padding="@(new(1, 0, 0, 0))">
	<Rows>
		@if (_messages.Count == 0)
		{
			<Markup Content="No messages yet. Type a message below to start chatting." Foreground="@Color.Grey" />
		}
		else
		{
			foreach (var message in _messages)
			{
				<Padder Padding="@(new(0, 1, 0, 0))">
					<Markup Content="@($"{(message.IsUser ? "You" : "Bot")}")" Foreground="@(message.IsUser? Color.Green: Color.Blue)" />
					<Markup Content="@message.Content" Foreground="@(message.IsUser? Color.Grey: Color.Default)" />
				</Padder>
			}
		}

		@if (_isProcessing)
		{
			<Padder Padding="@(new(0, 0, 0, 1))">
				<Columns>
					<RazorConsole.Components.Spinner SpinnerType="@Spectre.Console.Spinner.Known.Dots" />
					<Markup Content="AI is thinking..." Foreground="@Color.Grey" Decoration="@Decoration.Italic" />
				</Columns>
			</Padder>
		}
	</Rows>
</Padder>

<TextInput @bind-Value="_currentInput" Placeholder="Type your message here..." OnSubmit="SendMessage" Expand="true" />
<Padder Padding="@(new(1, 0, 0, 0))">
	<TextButton Content="Send" OnClick="SendMessage" BackgroundColor="@Color.Blue" FocusedColor="@Color.DodgerBlue1" />
</Padder>

@code {
	private List<ChatMessage> _messages = new();
	private string _currentInput = string.Empty;
	private bool _isProcessing = false;

	private async Task SendMessage()
	{
		if (string.IsNullOrWhiteSpace(_currentInput))
		{
			return;
		}

		var userMessage = _currentInput.Trim();
		_currentInput = string.Empty;

		_messages.Add(new ChatMessage
		{
			Content = userMessage,
			IsUser = true
		});

		StateHasChanged();

		_isProcessing = true;
		StateHasChanged();

		try
		{
			var response = await ChatService.SendMessageAsync(userMessage);

			_messages.Add(new ChatMessage
			{
				Content = response,
				IsUser = false
			});
		}
		catch (Exception ex)
		{
			_messages.Add(new ChatMessage
			{
				Content = $"[red]Error: {ex.Message}[/]",
				IsUser = false
			});
		}
		finally
		{
			_isProcessing = false;
			StateHasChanged();
		}
	}

	public class ChatMessage
	{
		public required string Content { get; set; }
		public bool IsUser { get; set; }
	}
}

Directory.Build.props

다른 설정 없이 아래와 같이 파일을 만들어둔 것만으로 Razor SDK의 내장 설정 덕분에 .razor 파일이 자동으로 불러와집니다.

<Project>
    <ItemGroup>
    </ItemGroup>
</Project>

Program.cs

#!/usr/bin/env dotnet

// Source: https://github.com/LittleLittleCloud/RazorConsole/tree/main/examples/LLMAgentTUI

#:sdk Microsoft.NET.Sdk.Razor

#:property OutputType=Exe
#:package RazorConsole.Core@0.0.5
#:package Microsoft.Extensions.AI@9.10.0
#:package Microsoft.Extensions.AI.Ollama@9.7.0-preview.1.25356.2
#:package Microsoft.Extensions.AI.OpenAI@9.10.0-preview.1.25513.3
#:package Microsoft.Extensions.Hosting@10.0.0-rc.2.25502.107
#:package Spectre.Console@0.52.1-preview.0.5

using Microsoft.Extensions.AI;
using LLMAgentTUI.Components;
using Microsoft.Extensions.DependencyInjection;
using OpenAI;
using RazorConsole.Core;

// Get API key from environment variable or use Ollama as default
var useOllama = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

await AppHost.RunAsync<App>(null, builder =>
{
    builder.ConfigureServices(services =>
    {
        if (useOllama)
        {
            // Use Ollama with local model
            services.AddChatClient(client =>
                new OllamaChatClient(new Uri("http://localhost:11434"), "gpt-oss:20b"));
        }
        else
        {
            // Use OpenAI
            var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
            services.AddChatClient(client =>
                new OpenAIClient(apiKey).GetChatClient("gpt-5-mini").AsIChatClient());
        }

        services.AddSingleton<ChatService>();
    });

    builder.Configure(options =>
    {
        options.AutoClearConsole = false;
    });
});

public sealed class ChatService
{
    private readonly IChatClient _chatClient;
    private readonly List<ChatMessage> _conversationHistory = new();

    public ChatService(IChatClient chatClient)
    {
        _chatClient = chatClient;
    }

    public async Task<string> SendMessageAsync(string message)
    {
        _conversationHistory.Add(new ChatMessage(ChatRole.User, message));

        var response = await _chatClient.GetResponseAsync(_conversationHistory).ConfigureAwait(false);

        var assistantMessage = response.Text ?? "No response from the AI.";
        _conversationHistory.Add(new ChatMessage(ChatRole.Assistant, assistantMessage));

        return assistantMessage;
    }
}

이와 같이 구성한 후, Ollama 또는 OpenAI API 키 발급 후 OPENAI_API_KEY 환경 변수에 대입하여 아래 명령어를 실행하면 스크린샷처럼 UI가 잘 꾸며진 TUI 프로그램이 만들어집니다. :+1:

dotnet run --no-cahce .\Program.cs
7개의 좋아요

저도 잠깐 만져봤는데, RazorConsole 자체 보다는 Razor 의 신묘함이 더 크게 느껴지는 것 같습니다.
누가 만들었는지 참 좋은 아이디어같습니다.

3개의 좋아요