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 { ["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 list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "path"; span[1] = "title"; span[2] = "body"; obj.Required = list; return obj; } } public async Task 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(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder); handler.AppendLiteral(""); handler.AppendFormatted(Escape(title)); handler.AppendLiteral(""); stringBuilder2.AppendLine(ref handler); stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder); handler.AppendLiteral(""); stringBuilder3.AppendLine(ref handler); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("
"); if (!string.IsNullOrEmpty(coverHtml)) { sb.AppendLine(coverHtml); } else { stringBuilder = sb; StringBuilder stringBuilder4 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder); handler.AppendLiteral("

"); handler.AppendFormatted(Escape(title)); handler.AppendLiteral("

"); stringBuilder4.AppendLine(ref handler); stringBuilder = sb; StringBuilder stringBuilder5 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(41, 2, stringBuilder); handler.AppendLiteral("
생성: "); handler.AppendFormatted(DateTime.Now, "yyyy-MM-dd HH:mm"); handler.AppendLiteral(" | AX Copilot"); handler.AppendFormatted(moodLabel); handler.AppendLiteral("
"); stringBuilder5.AppendLine(ref handler); } if (!string.IsNullOrEmpty(tocHtml)) { sb.AppendLine(tocHtml); } sb.AppendLine(body); sb.AppendLine("
"); sb.AppendLine(""); sb.AppendLine(""); await File.WriteAllTextAsync(fullPath, sb.ToString(), Encoding.UTF8, ct); List features = new List(); 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=\"([^\"]+)\"[^>]*>(.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline); if (matchCollection.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(""); 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("
"); stringBuilder3.AppendLine(ref handler); stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); handler.AppendLiteral("

"); handler.AppendFormatted(Escape(s)); handler.AppendLiteral("

"); stringBuilder4.AppendLine(ref handler); if (!string.IsNullOrEmpty(text)) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder5 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(34, 1, stringBuilder2); handler.AppendLiteral("
"); handler.AppendFormatted(Escape(text)); handler.AppendLiteral("
"); stringBuilder5.AppendLine(ref handler); } stringBuilder.AppendLine("
"); List list = new List(); 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("
"); handler.AppendFormatted(Escape(string.Join(" · ", list))); handler.AppendLiteral("
"); stringBuilder6.AppendLine(ref handler); stringBuilder.AppendLine("
"); return stringBuilder.ToString(); } private static string Escape(string s) { return s.Replace("&", "&").Replace("<", "<").Replace(">", ">"); } }