Skip to content

Commit

Permalink
feat: Generate HTML with feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
skarllot committed Dec 19, 2024
1 parent dd9ff31 commit 0f1f1f9
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageVersion Include="LibGit2Sharp" Version="0.31.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageVersion Include="Raiqub.Generators.EnumUtilities" Version="1.9.21" />
<PackageVersion Include="Raiqub.Generators.T4CodeWriter" Version="1.0.64" />
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<PackageVersion Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.7" />
</ItemGroup>
Expand Down
94 changes: 94 additions & 0 deletions src/FlowReviewer/Agent/ReviewChanges/FeedbackHtmlTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version: 16.0.0.0
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
namespace Ciandt.FlowTools.FlowReviewer.Agent.ReviewChanges
{
using System.Collections.Immutable;
using System.Text;
using Ciandt.FlowTools.FlowReviewer.Agent.ReviewChanges.v1;
using Ciandt.FlowTools.FlowReviewer.Common;
using Raiqub.Generators.T4CodeWriter;
using System;

/// <summary>
/// Class to produce the template output
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "16.0.0.0")]
public partial class FeedbackHtmlTemplate : CodeWriterBase<ImmutableList<ReviewerFeedbackResponse>>
{
/// <summary>
/// Create the template output
/// </summary>
public override string TransformText()
{
this.Write("<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <meta charset=\"utf-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n <title>Code Review Feedback</title>\r\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css\">\r\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/default.min.css\">\r\n <script src=\"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js\"></script>\r\n </head>\r\n <body>\r\n <section class=\"section\">\r\n <div class=\"container\">\r\n <h1 class=\"title\">Code Review Feedback</h1>\r\n <div class=\"content\">\r\n");

foreach (var response in Model)
{

this.Write(" <div class=\"box\">\r\n <h2 class=\"subtitle\">\r\n <span class=\"tag");

this.Write(this.ToStringHelper.ToStringWithCulture(GetRiskLevelCssClass(response.RiskScore)));

#line default
#line hidden
this.Write("\">Risk Score: ");

this.Write(this.ToStringHelper.ToStringWithCulture(response.RiskScore));

#line default
#line hidden
this.Write("</span> ");

this.Write(this.ToStringHelper.ToStringWithCulture(response.RiskDescription));

#line default
#line hidden
this.Write("\r\n </h2>\r\n <p><strong>File:</strong> ");

this.Write(this.ToStringHelper.ToStringWithCulture(response.Path));

#line default
#line hidden
this.Write(":");

this.Write(this.ToStringHelper.ToStringWithCulture(response.Line));

#line default
#line hidden
this.Write("</p>\r\n <div class=\"notification\">\r\n ");

this.Write(this.ToStringHelper.ToStringWithCulture(HtmlFormatter.EncodeToHtml(response.Feedback)));

#line default
#line hidden
this.Write("\r\n </div>\r\n </div>\r\n");

}

this.Write(" </div>\r\n </div>\r\n </section>\r\n <script>hljs.highlightAll();</script>\r\n </body>\r\n</html>\r\n");
return this.GenerationEnvironment.ToString();
}

public FeedbackHtmlTemplate(ImmutableList<ReviewerFeedbackResponse> model) : base(new StringBuilder())
{
Model = model;
}

public override string GetFileName() => Guid.NewGuid().ToString("N");

private static string GetRiskLevelCssClass(int number) => number switch{
0 => " is-white",
1 => " is-success",
2 => " is-warning",
3 => " is-danger",
_ => ""};

}
}
58 changes: 58 additions & 0 deletions src/FlowReviewer/Agent/ReviewChanges/FeedbackHtmlTemplate.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<#@ template language="C#" debug="false" linePragmas="false" hostspecific="false" inherits="CodeWriterBase<ImmutableList<ReviewerFeedbackResponse>>" #>
<#@ import namespace="System.Collections.Immutable" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="Ciandt.FlowTools.FlowReviewer.Agent.ReviewChanges.v1" #>
<#@ import namespace="Ciandt.FlowTools.FlowReviewer.Common" #>
<#@ import namespace="Raiqub.Generators.T4CodeWriter" #>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Code Review Feedback</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/default.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Code Review Feedback</h1>
<div class="content">
<#
foreach (var response in Model)
{
#>
<div class="box">
<h2 class="subtitle">
<span class="tag<#= GetRiskLevelCssClass(response.RiskScore) #>">Risk Score: <#= response.RiskScore #></span> <#= response.RiskDescription #>
</h2>
<p><strong>File:</strong> <#= response.Path #>:<#= response.Line #></p>
<div class="notification">
<#= HtmlFormatter.EncodeToHtml(response.Feedback) #>
</div>
</div>
<#
}
#>
</div>
</div>
</section>
<script>hljs.highlightAll();</script>
</body>
</html>
<#+
public FeedbackHtmlTemplate(ImmutableList<ReviewerFeedbackResponse> model) : base(new StringBuilder())
{
Model = model;
}

public override string GetFileName() => Guid.NewGuid().ToString("N");

private static string GetRiskLevelCssClass(int number) => number switch{
0 => " is-white",
1 => " is-success",
2 => " is-warning",
3 => " is-danger",
_ => ""};
#>
60 changes: 50 additions & 10 deletions src/FlowReviewer/Agent/ReviewChanges/FlowChangesReviewer.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO.Abstractions;
using System.Text;
using System.Text.Json;
using AutomaticInterface;
using Ciandt.FlowTools.FlowReviewer.Agent.ReviewChanges.v1;
using Ciandt.FlowTools.FlowReviewer.ChangeTracking;
using Ciandt.FlowTools.FlowReviewer.Common;
using Ciandt.FlowTools.FlowReviewer.Flow.ProxyCompleteChat;
using Ciandt.FlowTools.FlowReviewer.Flow.ProxyCompleteChat.v1;
using Ciandt.FlowTools.FlowReviewer.Persistence;
using Spectre.Console;

namespace Ciandt.FlowTools.FlowReviewer.Agent.ReviewChanges;
Expand All @@ -28,23 +31,35 @@ public Option<Unit> Run(ImmutableList<FileChange> changes)
.Where(g => g.Key.IsSome)
.Select(g => new { Instructions = g.Key.Unwrap(), Diff = g.AggregateToStringLines(c => c.Diff) })
.SelectMany(x => GetFeedback([AllowedModel.Claude35Sonnet, AllowedModel.Gpt4o], x.Diff, x.Instructions))
.OrderByDescending(x => x.RiskScore).ThenBy(x => x.Path, StringComparer.OrdinalIgnoreCase)
.ToImmutableList();

console.WriteLine($"Created {feedback.Count} comments");

if (feedback.Count > 0)
{
var tempPath = ApplicationData.GetTempPath(fileSystem);
fileSystem.Directory.CreateDirectory(tempPath);
var feedbackFilePath = fileSystem.Path.Combine(
fileSystem.Directory.GetCurrentDirectory(),
$"{DateTime.UtcNow:yyyyMMddHHmmss}-feedback.json");
tempPath,
$"{DateTime.UtcNow:yyyyMMddHHmmss}-feedback.html");

using var stream = fileSystem.File.Open(feedbackFilePath, FileMode.Create);
JsonSerializer.Serialize(stream, feedback, jsonContext.ImmutableListReviewerFeedbackResponse);
var htmlContent = new FeedbackHtmlTemplate(feedback).TransformText();
fileSystem.File.WriteAllText(feedbackFilePath, htmlContent, Encoding.UTF8);

OpenHtmlFile(feedbackFilePath);
}

return Unit();
}

private static void OpenHtmlFile(string filePath)
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo { FileName = filePath, UseShellExecute = true };
process.Start();
}

private ImmutableList<ReviewerFeedbackResponse> GetFeedback(
IEnumerable<AllowedModel> allowedModels,
string diff,
Expand All @@ -70,12 +85,37 @@ private ImmutableList<ReviewerFeedbackResponse> GetFeedback(
return [];
}

var content = result.Unwrap().Content;
var feedback = content.Contains('[') && content.Contains(']')
? JsonSerializer.Deserialize(
content.AsSpan()[content.IndexOf('[')..(content.LastIndexOf(']') + 1)],
jsonContext.ImmutableListReviewerFeedbackResponse) ?? []
: [];
var feedback = TryDeserializeFeedback(result.Unwrap().Content);
return feedback;
}

private ImmutableList<ReviewerFeedbackResponse> TryDeserializeFeedback(ReadOnlySpan<char> content)
{
if (content.IsWhiteSpace())
return [];

while (!content.IsEmpty)
{
var start = content.IndexOf('[');
if (start < 0)
return [];

var end = content.IndexOf(']');
if (end < start)
return [];

try
{
return JsonSerializer.Deserialize(
content[start..(end + 1)],
jsonContext.ImmutableListReviewerFeedbackResponse) ?? [];
}
catch (JsonException)
{
content = content[(end + 1)..];
}
}

return [];
}
}
2 changes: 1 addition & 1 deletion src/FlowReviewer/Agent/ReviewChanges/Instructions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ It is not necessary to add low-risk comments that are not relevant to changes in
The message in the field text must be in English.
Format the response in a valid JSON format as a list of feedbacks. The feedback must not contains any block formatting and line breakers. Remember it is crucial that the result has the file path.
Format the response in a valid JSON format as a list of feedbacks. Remember it is crucial that the result has the file path.
This valid JSON is going to be inserted in a value of a key-value from another JSON object, be-aware about the formatting. Remember to only list feedbacks that needs user action.
The schema of the JSON feedback object must be:
```json
Expand Down
58 changes: 58 additions & 0 deletions src/FlowReviewer/Common/HtmlFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace Ciandt.FlowTools.FlowReviewer.Common;

public static partial class HtmlFormatter
{
public static string EncodeToHtml(string text)
{
text = HttpUtility.HtmlEncode(text);
text = CodeDelimiterRegex().Replace(text, """<pre><code class="language-$1">$2</code></pre>""");
text = CodeBlockDelimiterRegex().Replace(text, "<code>$1</code>");
text = ReplaceLineBreaksExceptInCode(text);
return text;
}

private static string ReplaceLineBreaksExceptInCode(string input)
{
if (string.IsNullOrEmpty(input))
return input;

var result = new StringBuilder();
var insideCodeBlock = false;
var lastSize = 0;

foreach (var line in input.AsSpan().EnumerateLines())
{
if (line.Contains("<code", StringComparison.OrdinalIgnoreCase))
insideCodeBlock = true;

if (!insideCodeBlock)
{
result.Append(line.TrimEnd());
result.Append("<br>");
lastSize = 4;
}
else
{
result.Append(line);
result.Append('\n');
lastSize = 1;
}

if (line.Contains("</code>", StringComparison.OrdinalIgnoreCase))
insideCodeBlock = false;
}

result.Length -= lastSize;
return result.ToString();
}

[GeneratedRegex(@"```(\S+)?(.*?)```", RegexOptions.Multiline)]
private static partial Regex CodeDelimiterRegex();

[GeneratedRegex("`(.*?)`", RegexOptions.Singleline)]
private static partial Regex CodeBlockDelimiterRegex();
}
16 changes: 16 additions & 0 deletions src/FlowReviewer/FlowReviewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,24 @@
<PackageReference Include="LibGit2Sharp" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Raiqub.Generators.EnumUtilities" />
<PackageReference Include="Raiqub.Generators.T4CodeWriter" />
<PackageReference Include="Spectre.Console" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" />
</ItemGroup>

<ItemGroup>
<Compile Update="Agent\ReviewChanges\FeedbackHtmlTemplate.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>FeedbackHtmlTemplate.tt</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<None Update="Agent\ReviewChanges\FeedbackHtmlTemplate.tt">
<Generator>TextTemplatingFilePreprocessor</Generator>
<LastGenOutput>FeedbackHtmlTemplate.cs</LastGenOutput>
</None>
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions src/FlowReviewer/Persistence/ApplicationData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ public static class ApplicationData
public static string GetPath(IFileSystem fileSystem) => fileSystem.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
AppName);

public static string GetTempPath(IFileSystem fileSystem) => fileSystem.Path.Combine(
fileSystem.Path.GetTempPath(),
AppName);
}
Loading

0 comments on commit 0f1f1f9

Please sign in to comment.