337 lines
13 KiB
C#
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("&", "&").Replace("<", "<").Replace(">", ">");
|
|
}
|
|
}
|