Files
AX-Copilot-Codex/.decompiledproj/AxCopilot/Services/Agent/HtmlSkill.cs

337 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace AxCopilot.Services.Agent;
public class HtmlSkill : IAgentTool
{
public string Name => "html_create";
public string Description => "Create a styled HTML (.html) document with rich formatting. Supports: table of contents (toc), cover page, callouts (.callout-info/warning/tip/danger), badges (.badge-blue/green/red/yellow/purple), CSS bar charts (.chart-bar), progress bars (.progress), timelines (.timeline), grid layouts (.grid-2/3/4), and auto section numbering. Available moods: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard.";
public ToolParameterSchema Parameters
{
get
{
ToolParameterSchema obj = new ToolParameterSchema
{
Properties = new Dictionary<string, ToolProperty>
{
["path"] = new ToolProperty
{
Type = "string",
Description = "Output file path (.html). Relative to work folder."
},
["title"] = new ToolProperty
{
Type = "string",
Description = "Document title (shown in browser tab and header)"
},
["body"] = new ToolProperty
{
Type = "string",
Description = "HTML body content. Use semantic tags: h2/h3 for sections, div.callout-info/warning/tip/danger for callouts, span.badge-blue/green/red for badges, div.chart-bar>div.bar-item for charts, div.grid-2/3/4 for grid layouts, div.timeline>div.timeline-item for timelines, div.progress for progress bars."
},
["mood"] = new ToolProperty
{
Type = "string",
Description = "Design template mood: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard. Default: modern"
},
["style"] = new ToolProperty
{
Type = "string",
Description = "Optional additional CSS. Appended after mood+shared CSS."
},
["toc"] = new ToolProperty
{
Type = "boolean",
Description = "Auto-generate table of contents from h2/h3 headings. Default: false"
},
["numbered"] = new ToolProperty
{
Type = "boolean",
Description = "Auto-number h2/h3 sections (1., 1-1., etc). Default: false"
},
["cover"] = new ToolProperty
{
Type = "object",
Description = "Cover page config: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\", \"date\": \"...\", \"gradient\": \"#hex1,#hex2\"}. Omit to skip cover page."
}
}
};
int num = 3;
List<string> list = new List<string>(num);
CollectionsMarshal.SetCount(list, num);
Span<string> span = CollectionsMarshal.AsSpan(list);
span[0] = "path";
span[1] = "title";
span[2] = "body";
obj.Required = list;
return obj;
}
}
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
{
string path = args.GetProperty("path").GetString() ?? "";
string title = args.GetProperty("title").GetString() ?? "Report";
string body = args.GetProperty("body").GetString() ?? "";
JsonElement s;
string customStyle = (args.TryGetProperty("style", out s) ? s.GetString() : null);
JsonElement m;
string mood = (args.TryGetProperty("mood", out m) ? (m.GetString() ?? "modern") : "modern");
JsonElement tocVal;
bool useToc = args.TryGetProperty("toc", out tocVal) && tocVal.GetBoolean();
JsonElement numVal;
bool useNumbered = args.TryGetProperty("numbered", out numVal) && numVal.GetBoolean();
JsonElement coverVal;
bool hasCover = args.TryGetProperty("cover", out coverVal) && coverVal.ValueKind == JsonValueKind.Object;
string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
if (context.ActiveTab == "Cowork")
{
fullPath = AgentContext.EnsureTimestampedPath(fullPath);
}
if (!fullPath.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && !fullPath.EndsWith(".htm", StringComparison.OrdinalIgnoreCase))
{
fullPath += ".html";
}
if (!context.IsPathAllowed(fullPath))
{
return ToolResult.Fail("경로 접근 차단: " + fullPath);
}
if (!(await context.CheckWritePermissionAsync(Name, fullPath)))
{
return ToolResult.Fail("쓰기 권한 거부: " + fullPath);
}
try
{
string dir = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
string style = TemplateService.GetCss(mood);
if (!string.IsNullOrEmpty(customStyle))
{
style = style + "\n" + customStyle;
}
TemplateMood moodInfo = TemplateService.GetMood(mood);
string moodLabel = ((moodInfo != null) ? (" · " + moodInfo.Icon + " " + moodInfo.Label) : "");
if (useNumbered)
{
body = AddNumberedClass(body);
}
body = EnsureHeadingIds(body);
string tocHtml = (useToc ? GenerateToc(body) : "");
string coverHtml = (hasCover ? GenerateCover(coverVal, title) : "");
StringBuilder sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"ko\">");
sb.AppendLine("<head>");
sb.AppendLine("<meta charset=\"UTF-8\">");
sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
StringBuilder stringBuilder = sb;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder);
handler.AppendLiteral("<title>");
handler.AppendFormatted(Escape(title));
handler.AppendLiteral("</title>");
stringBuilder2.AppendLine(ref handler);
stringBuilder = sb;
StringBuilder stringBuilder3 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder);
handler.AppendLiteral("<style>");
handler.AppendFormatted(style);
handler.AppendLiteral("</style>");
stringBuilder3.AppendLine(ref handler);
sb.AppendLine("</head>");
sb.AppendLine("<body>");
sb.AppendLine("<div class=\"container\">");
if (!string.IsNullOrEmpty(coverHtml))
{
sb.AppendLine(coverHtml);
}
else
{
stringBuilder = sb;
StringBuilder stringBuilder4 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder);
handler.AppendLiteral("<h1>");
handler.AppendFormatted(Escape(title));
handler.AppendLiteral("</h1>");
stringBuilder4.AppendLine(ref handler);
stringBuilder = sb;
StringBuilder stringBuilder5 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(41, 2, stringBuilder);
handler.AppendLiteral("<div class=\"meta\">생성: ");
handler.AppendFormatted(DateTime.Now, "yyyy-MM-dd HH:mm");
handler.AppendLiteral(" | AX Copilot");
handler.AppendFormatted(moodLabel);
handler.AppendLiteral("</div>");
stringBuilder5.AppendLine(ref handler);
}
if (!string.IsNullOrEmpty(tocHtml))
{
sb.AppendLine(tocHtml);
}
sb.AppendLine(body);
sb.AppendLine("</div>");
sb.AppendLine("</body>");
sb.AppendLine("</html>");
await File.WriteAllTextAsync(fullPath, sb.ToString(), Encoding.UTF8, ct);
List<string> features = new List<string>();
if (useToc)
{
features.Add("목차");
}
if (useNumbered)
{
features.Add("섹션번호");
}
if (hasCover)
{
features.Add("커버페이지");
}
string featureStr = ((features.Count > 0) ? (" [" + string.Join(", ", features) + "]") : "");
return ToolResult.Ok($"HTML 문서 생성 완료: {fullPath} (디자인: {mood}{featureStr})", fullPath);
}
catch (Exception ex)
{
return ToolResult.Fail("HTML 생성 실패: " + ex.Message);
}
}
private static string EnsureHeadingIds(string html)
{
int counter = 0;
return Regex.Replace(html, "<(h[23])(\\s[^>]*)?>", delegate(Match match)
{
string value = match.Groups[1].Value;
string value2 = match.Groups[2].Value;
counter++;
return (!value2.Contains("id=", StringComparison.OrdinalIgnoreCase)) ? $"<{value}{value2} id=\"section-{counter}\">" : match.Value;
});
}
private static string AddNumberedClass(string html)
{
return Regex.Replace(html, "<(h[23])(\\s[^>]*)?>", delegate(Match match)
{
string value = match.Groups[1].Value;
string value2 = match.Groups[2].Value;
if (value2.Contains("numbered", StringComparison.OrdinalIgnoreCase))
{
return match.Value;
}
return Regex.IsMatch(value2, "class\\s*=\\s*\"", RegexOptions.IgnoreCase) ? Regex.Replace(match.Value, "class\\s*=\\s*\"", "class=\"numbered ") : ("<" + value + value2 + " class=\"numbered\">");
});
}
private static string GenerateToc(string html)
{
MatchCollection matchCollection = Regex.Matches(html, "<(h[23])[^>]*id=\"([^\"]+)\"[^>]*>(.*?)</\\1>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (matchCollection.Count == 0)
{
return "";
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("<nav class=\"toc\">");
stringBuilder.AppendLine("<h2>\ud83d\udccb 목차</h2>");
stringBuilder.AppendLine("<ul>");
foreach (Match item in matchCollection)
{
string text = item.Groups[1].Value.ToLower();
string value = item.Groups[2].Value;
string value2 = Regex.Replace(item.Groups[3].Value, "<[^>]+>", "").Trim();
string value3 = ((text == "h3") ? " class=\"toc-h3\"" : "");
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(25, 3, stringBuilder2);
handler.AppendLiteral("<li");
handler.AppendFormatted(value3);
handler.AppendLiteral("><a href=\"#");
handler.AppendFormatted(value);
handler.AppendLiteral("\">");
handler.AppendFormatted(value2);
handler.AppendLiteral("</a></li>");
stringBuilder2.AppendLine(ref handler);
}
stringBuilder.AppendLine("</ul>");
stringBuilder.AppendLine("</nav>");
return stringBuilder.ToString();
}
private static string GenerateCover(JsonElement cover, string fallbackTitle)
{
JsonElement value;
string s = (cover.TryGetProperty("title", out value) ? (value.GetString() ?? fallbackTitle) : fallbackTitle);
JsonElement value2;
string text = (cover.TryGetProperty("subtitle", out value2) ? (value2.GetString() ?? "") : "");
JsonElement value3;
string text2 = (cover.TryGetProperty("author", out value3) ? (value3.GetString() ?? "") : "");
JsonElement value4;
string item = (cover.TryGetProperty("date", out value4) ? (value4.GetString() ?? DateTime.Now.ToString("yyyy-MM-dd")) : DateTime.Now.ToString("yyyy-MM-dd"));
JsonElement value5;
string text3 = (cover.TryGetProperty("gradient", out value5) ? value5.GetString() : null);
string value6 = "";
if (!string.IsNullOrEmpty(text3) && text3.Contains(','))
{
string[] array = text3.Split(',');
value6 = $" style=\"background: linear-gradient(135deg, {array[0].Trim()} 0%, {array[1].Trim()} 100%)\"";
}
StringBuilder stringBuilder = new StringBuilder();
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(24, 1, stringBuilder2);
handler.AppendLiteral("<div class=\"cover-page\"");
handler.AppendFormatted(value6);
handler.AppendLiteral(">");
stringBuilder3.AppendLine(ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2);
handler.AppendLiteral("<h1>");
handler.AppendFormatted(Escape(s));
handler.AppendLiteral("</h1>");
stringBuilder4.AppendLine(ref handler);
if (!string.IsNullOrEmpty(text))
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(34, 1, stringBuilder2);
handler.AppendLiteral("<div class=\"cover-subtitle\">");
handler.AppendFormatted(Escape(text));
handler.AppendLiteral("</div>");
stringBuilder5.AppendLine(ref handler);
}
stringBuilder.AppendLine("<div class=\"cover-divider\"></div>");
List<string> list = new List<string>();
if (!string.IsNullOrEmpty(text2))
{
list.Add(text2);
}
list.Add(item);
list.Add("AX Copilot");
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(30, 1, stringBuilder2);
handler.AppendLiteral("<div class=\"cover-meta\">");
handler.AppendFormatted(Escape(string.Join(" · ", list)));
handler.AppendLiteral("</div>");
stringBuilder6.AppendLine(ref handler);
stringBuilder.AppendLine("</div>");
return stringBuilder.ToString();
}
private static string Escape(string s)
{
return s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
}
}