Skip to content

Commit

Permalink
Merge pull request #35 from andresharpe/feature/cute-chat-markdown-su…
Browse files Browse the repository at this point in the history
…pport

Added markdown support for general chat
  • Loading branch information
andresharpe authored Sep 30, 2024
2 parents 3961f7f + e329664 commit ce1fc0e
Show file tree
Hide file tree
Showing 30 changed files with 1,242 additions and 9 deletions.
55 changes: 55 additions & 0 deletions source/Cute.Lib/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,61 @@ public static string Snip(this string text, int snipTo)
return text[..(snipTo - 1)] + "..";
}

public static IEnumerable<string> GetFixedLines(this ReadOnlySpan<char> input, int maxLineLength = 80)
{
var lines = new List<string>();

while (!input.IsEmpty)
{
// Find the maximum slice we can take for this line
var length = Math.Min(maxLineLength, input.Length);
var slice = input[..length]; // Using the range operator here for slicing

// Find the first occurrence of \r or \n
var firstNewlineIndex = slice.IndexOfAny('\r', '\n');
// Find the last occurrence of a space
var lastSpaceIndex = slice.LastIndexOf(' ');

if (lastSpaceIndex != -1 && firstNewlineIndex > lastSpaceIndex)
{
lastSpaceIndex = -1;
}

if (lastSpaceIndex != -1 && length < maxLineLength)
{
lastSpaceIndex = -1;
}

// Break at the first newline character
if (firstNewlineIndex != -1 && (firstNewlineIndex < lastSpaceIndex || lastSpaceIndex == -1))
{
lines.Add(slice[..firstNewlineIndex].ToString());

// Handle \r\n as a single line break
if (firstNewlineIndex + 1 < input.Length && input[firstNewlineIndex] == '\r' && input[firstNewlineIndex + 1] == '\n')
input = input[(firstNewlineIndex + 2)..]; // Skip \r\n using range
else
input = input[(firstNewlineIndex + 1)..]; // Skip \r or \n using range

continue;
}

// Break at the last space if no newline is found earlier
if (lastSpaceIndex != -1)
{
lines.Add(slice[..lastSpaceIndex].ToString());
input = input[(lastSpaceIndex + 1)..]; // Skip the space using range
continue;
}

// If no space or newline was found, just break at max length
lines.Add(slice.ToString());
input = input[length..]; // Move to the next chunk using range operator
}

return lines;
}

[GeneratedRegex(@"\s+")]
private static partial Regex MultiWhiteSpace();

Expand Down
40 changes: 32 additions & 8 deletions source/Cute/Commands/Chat/ChatCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Cute.Lib.Extensions;
using Cute.Services;
using Cute.Services.CliCommandInfo;
using Cute.Services.Markdown;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenAI.Chat;
Expand Down Expand Up @@ -182,12 +183,13 @@ await AnsiConsole.Status()
AnsiConsole.MarkupLine(_console.FormatToMarkup($"Presence Penalty : {chatCompletionOptions.PresencePenalty}", Globals.StyleDim, Globals.StyleSubHeading));
_console.WriteBlankLine();
_console.WriteRuler();
_console.WriteBlankLine();
}

_console.WriteBlankLine();

if (settings.Key is not null)
{
_console.WriteNormalWithHighlights($"Press {"<Enter>"} on a blank line to submit your prompt. (i.e. type your prompt and press {"<Enter>"} twice to submit your prompt).", Globals.StyleHeading);
_console.WriteNormalWithHighlights($"Press {"<Enter>"} on a blank line to submit your prompt. (i.e. type your prompt and press {"<Enter>"} twice).", Globals.StyleHeading);
_console.WriteBlankLine();
}

Expand Down Expand Up @@ -240,7 +242,25 @@ await AnsiConsole.Status()

_console.WriteBlankLine();

if (botResponse.Answer is not null) _console.WriteSubHeading(botResponse.Answer);
if (botResponse.Answer is not null)
{
if (settings.Key == null)
{
_console.WriteSubHeading(botResponse.Answer);
}
else
{
MarkdownConsole.Write(botResponse.Answer);
}
}

if (botResponse.Type == "Exit")
{
_console.WriteBlankLine();
_console.WriteAlert($"{SayBye()}!");
_console.WriteBlankLine();
break;
}

if (botResponse.Question is not null && botResponse.Question.Contains("Shall we give it a shot?"))
{
Expand All @@ -249,8 +269,11 @@ await AnsiConsole.Status()
continue;
}

_console.WriteBlankLine();
if (botResponse.Question is not null) _console.WriteSubHeading(botResponse.Question);
if (botResponse.Question is not null)
{
_console.WriteBlankLine();
_console.WriteSubHeading(botResponse.Question);
}
}

return 0;
Expand Down Expand Up @@ -380,7 +403,8 @@ When asked your name you always quote something profound from HHGTTG.
"answer" is a JSON string and contains your best answer. Keep them punchy.
"question" is a JSON string and contains your next question for the user to help them reach their goal.
"queryOrCommand" is a JSON string and contains the accurate CLI command or GraphQl query that will achieve the goal.
"type" contains "GraphQL" or "CLI" depending on what is in "queryOrCommand".
"type" contains "GraphQL" or "CLI" to execte commands depending on what is in "queryOrCommand".
"type" contains "Exit" if the user wants to leave the conversation or quit the app.
"queryOrCommand" and "type" MUST only supplied when you ask "Shall we give it a shot?" when the goal is clear to you.
These fields MUST always contain valid JSON strings. No other data types are allowed in them.
Only output the JSON structure, one object per response.
Expand Down Expand Up @@ -436,13 +460,13 @@ If a fiels is a Link to another entry then the child entry fields are valid to u
When generating CLI commands with parameters, the use of single quotes (') to delimit a string is NOT supported.
Always use doube quotes (") to delimit strings for commands and escape any double quotes with a slash (\")
Never escape double quotes unless they are inside unescaped quotes on the command line
Regex expressions are currently NOT supported in edit or find or replace expressions. Don't suggest them
Regex expressions are NOT supported in edit or find or replace expressions. Don't suggest them ever.
"""";
}

private static string BuildContentTypesPromptInfo(IEnumerable<ContentType> contentTypes)
{
string[] excludePrefix = ["ux", "ui", "cute", "meta"];
string[] excludePrefix = ["ux", "ui", "meta"];

var sbContentTypesInfo = new StringBuilder();
foreach (var contentType in contentTypes.OrderBy(ct => ct.Id()))
Expand Down
2 changes: 1 addition & 1 deletion source/Cute/Commands/_Legacy/WebCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ private async Task DisplaySettings(HttpContext context)
private const string _prettifyColors = """
<style>
.atv,.str{color:#ec7600}.kwd{color:#93c763}.com{color:#66747b}.typ{color:#678cb1}.lit{color:#facd22}.pln,.pun{color:#f1f2f3}.tag{color:#8ac763}
.atn{color:#e0e2e4}.dec{color:purple}pre.prettyprint{border:0 solid #888}ol.linenums{margin-top:0;margin-bottom:0}.prettyprint{background:#000}
.atn{color:#e0e2e4}.dec{color:{_highlighted}}pre.prettyprint{border:0 solid #888}ol.linenums{margin-top:0;margin-bottom:0}.prettyprint{background:#000}
li.L0,li.L1,li.L2,li.L3,li.L4,li.L5,li.L6,li.L7,li.L8,li.L9{color:#555;list-style-type:decimal}
li.L1,li.L3,li.L5,li.L7,li.L9{background:#111}@media print{.kwd,.tag,.typ{font-weight:700}.atv,.str{color:#060}.kwd,.tag{color:#006}
.com{color:#600;font-style:italic}.typ{color:#404}.lit{color:#044}.pun{color:#440}.pln{color:#000}.atn{color:#404}}
Expand Down
2 changes: 2 additions & 0 deletions source/Cute/Cute.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="ReadLine" Version="2.0.1" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Spectre.Console.ImageSharp" Version="0.49.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
</ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions source/Cute/Services/Markdown/Converters/AnsiSupportConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Spectre.Console;

namespace Cute.Services.Markdown.Console.Converters;

internal static class AnsiSupportConverter
{
internal static AnsiSupport FromAnsiSupported(bool ansiSupported) =>
ansiSupported
? AnsiSupport.Yes
: AnsiSupport.No;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Cute.Services.Markdown.Console.Renderers;
using Spectre.Console;

namespace Cute.Services.Markdown.Console.Converters;

internal static class ColorSystemSupportConverter
{
internal static ColorSystemSupport FromColorSystem(ColorSystem colorSystem)
{
return (colorSystem) switch
{
ColorSystem.EightBit => ColorSystemSupport.EightBit,
ColorSystem.Legacy => ColorSystemSupport.Legacy,
ColorSystem.NoColors => ColorSystemSupport.NoColors,
ColorSystem.Standard => ColorSystemSupport.Standard,
ColorSystem.TrueColor => ColorSystemSupport.TrueColor,
_ => ColorSystemSupport.Detect
};
}
}
9 changes: 9 additions & 0 deletions source/Cute/Services/Markdown/Extensions/ObjectExtenions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace Cute.Services.Markdown.Console.Extensions;

public static class ObjectExtensions
{
public static string ToNotNullString(this object obj) =>
obj.ToString() ?? string.Empty;
}
24 changes: 24 additions & 0 deletions source/Cute/Services/Markdown/Formatters/NumberFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Cute.Services.Markdown.Console.Renderers.CharacterSets;

namespace Cute.Services.Markdown.Console.Formatters;

public class NumberFormatter
{
public string Format(int number, CharacterSet characterSet)
{
// TODO: There is no fallback if the users font does not support nerd fonts.
// Detecting support could be very hard.
// Nerd font may need to be a opt in/out feature.
return number.ToString()
.Replace("0", characterSet.Zero)
.Replace("1", characterSet.One)
.Replace("2", characterSet.Two)
.Replace("3", characterSet.Three)
.Replace("4", characterSet.Four)
.Replace("5", characterSet.Five)
.Replace("6", characterSet.Six)
.Replace("7", characterSet.Seven)
.Replace("8", characterSet.Eight)
.Replace("9", characterSet.Nine);
}
}
67 changes: 67 additions & 0 deletions source/Cute/Services/Markdown/MarkdownConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Cute.Constants;
using Cute.Services.Markdown.Console.Renderers;

namespace Cute.Services.Markdown;

/// <summary>
/// Renders markdown in the terminal.
/// </summary>
public static class MarkdownConsole
{
private const UseNerdFonts UseNerdFontsDefault = UseNerdFonts.No;
private static UseNerdFonts UseNerdFontsField = UseNerdFontsDefault;
private static AnsiRenderer DefaultRenderer = GetAnsiRenderer();

/// <summary>
/// Configure Nerd Fonts usage for special characters, like list bullets.
/// <see href="https://www.nerdfonts.com/">Nerd Fonts</see> are awesome, but not supported by all fonts.
/// </summary>
public static UseNerdFonts UseNerdFonts
{
get => UseNerdFontsField;
set
{
UseNerdFontsField = value;
DefaultRenderer = GetAnsiRenderer();
}
}

/// <summary>
/// Writes formatted markdown in the console.
/// </summary>
/// <param name="markdown">Markdown to format.</param>
public static void Write(string markdown)
{
DefaultRenderer.Write(markdown);
}

/// <summary>
/// Writes formatted markdown in the console.
/// </summary>
/// <param name="markdown">Markdown to format.</param>
/// <param name="writer">
/// Override the default console.
/// <remarks>
/// Useful for test and debugging only.
/// </remarks>
/// </param>
public static void Write(string markdown, TextWriter writer)
{
GetAnsiRenderer(writer).Write(markdown);
}

private static AnsiRenderer GetAnsiRenderer()
{
return new AnsiRendererBuilder()
.SetNerdFontsUsage(UseNerdFontsField)
.Build();
}

private static AnsiRenderer GetAnsiRenderer(TextWriter writer)
{
return new AnsiRendererBuilder()
.SetNerdFontsUsage(UseNerdFontsField)
.RedirectOutput(writer)
.Build();
}
}
39 changes: 39 additions & 0 deletions source/Cute/Services/Markdown/Options/FeatureFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Cute.Services.Markdown.Console.Options;

/// <summary>
/// Flags that modify the behaviour of the AnsiRenderer.
/// </summary>
internal static class FeatureFlags
{
private const string ThrowOnSupportedEnvironmentVariable = "MORELLO_MARKDOWN_CONSOLE_THROW_ON_UNSUPPORTED_TYPE";
private const string ForceBasicSyntaxHighlighterEnvironmentVariable = "MORELLO_MARKDOWN_CONSOLE_FORCE_BASIC_SYNTAX_HIGHLIGHTER";
private const string ForceAnsiColourEnvironmentVariable = "MORELLO_MARKDOWN_CONSOLE_FORCE_ANSI_COLOUR";
private const string True = "true";
private const string Yes = "yes";
private const string On = "on";
private const string Enabled = "enabled";
private const string Active = "active";

/// <summary>
/// Disables MarkdownConsole's default behaviour of falling back to plain text when it encounters
/// an unsupported Markdown type.
/// </summary>
public static bool ThrowOnUnsupportedMarkdownType =>
EnvironmentVariablePresentAndActive(ThrowOnSupportedEnvironmentVariable);

public static bool ForceBasicSyntaxHighlighter =>
EnvironmentVariablePresentAndActive(ForceBasicSyntaxHighlighterEnvironmentVariable);

public static bool ForceAnsiColour =>
EnvironmentVariablePresentAndActive(ForceAnsiColourEnvironmentVariable);

private static bool EnvironmentVariablePresentAndActive(string environmentVariableName)
{
var value = Environment
.GetEnvironmentVariable(environmentVariableName)?
.ToLower()
?? string.Empty;

return value is True or Yes or On or Enabled or Active;
}
}
24 changes: 24 additions & 0 deletions source/Cute/Services/Markdown/Parsers/MarkdownParsers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Markdig;
using Markdig.Syntax;

namespace Cute.Services.Markdown.Console.Parsers;

/// <summary>
/// Parses raw markdown text and returns an abstract-syntax-tree representation.
/// </summary>
public class MarkdownParser
{
private readonly static MarkdownPipeline _markdownPipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();

/// <summary>
/// Converts markdown into a document.
/// /// </summary>
/// <param name="markdown">Raw markdown text</param>
/// <returns>ABT markdown document</returns>
public MarkdownDocument ConvertToMarkdownDocument(string markdown)
{
return Markdig.Markdown.Parse(markdown, _markdownPipeline);
}
}
Loading

0 comments on commit ce1fc0e

Please sign in to comment.