?? ??? 3?? DOCX ????XLSX ?? ???HTML ???? ?? ??
- DocumentAssemblerTool? template_path/page_numbers? ??? DOCX ??? ??, ??????????? ?? ??? ?? - ExcelSkill? data_validations? ???? ??/??/?? ?? ??? ?? ??? ?? ?? ?? ?? - HtmlSkill? ArtifactQualityReviewService? decision_summary/evidence_cards ??? ?? ?? ?? ??? ?? - DocumentAssemblerDocxFeaturesTests, HtmlSkillConsultingSectionsTests, ExcelSkillDataValidationTests? ?? ??? ?? - README.md? docs/DEVELOPMENT.md? 2026-04-14 22:28 (KST) ???? ?? ??: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\ (?? 0 / ?? 0) - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\ (?? 9)
This commit is contained in:
10
README.md
10
README.md
@@ -1802,3 +1802,13 @@ MIT License
|
||||
- PPT 3? ?? ??? ??????. DeckPlanningService, DeckQualityReviewService, PptxSkill? planning/quality summary ??? ??? PPT ?? ?? ??? ???? ?? ??? ?? ?? ??? ??? ?????.
|
||||
- ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\ ?? 0 / ?? 0
|
||||
- ??: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\ ?? 5
|
||||
|
||||
업데이트: 2026-04-14 22:28 (KST)
|
||||
- 문서 포맷 고도화 3차를 반영했습니다. `DocumentAssemblerTool`은 DOCX 조립 시 `template_path`와 `page_numbers`를 지원해 기존 사내 Word 템플릿을 복제한 뒤 커버, 목차, 머리글, 바닥글, 페이지 번호를 함께 적용할 수 있습니다.
|
||||
- 같은 도구는 템플릿 상속 여부를 구조화 문서 품질 리뷰 입력에도 전달해, 결과 메시지에서 템플릿 기반 산출물의 강점과 보완 포인트를 함께 요약합니다.
|
||||
- `ExcelSkill`은 `data_validations` 파라미터를 지원하도록 확장됐습니다. 단일 시트, summary sheet 포함 워크북, 멀티 시트 워크북 모두에서 OpenXML `DataValidation` 규칙을 생성하고 품질 리뷰에 검증 규칙 수를 반영합니다.
|
||||
- `HtmlSkill`은 `decision_summary`, `evidence_cards` 섹션을 지원해 경영 보고형 HTML에서 의사결정 요약과 근거 카드 묶음을 구조화 블록으로 렌더링합니다.
|
||||
- `ArtifactQualityReviewService`의 HTML 리뷰도 새 블록을 인식해 comparison, roadmap, matrix 외에 decision/evidence 구조를 강점으로 계산하도록 보강했습니다.
|
||||
- 테스트로 `ExcelSkillDataValidationTests`를 추가했고, `DocumentAssemblerDocxFeaturesTests`, `HtmlSkillConsultingSectionsTests`를 확장해 DOCX 템플릿/페이지 번호와 HTML decision/evidence 블록을 회귀 검증했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\` 통과 9
|
||||
|
||||
@@ -872,3 +872,13 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
||||
- 테스트로 [DocumentPlannerWorkbookScaffoldTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentPlannerWorkbookScaffoldTests.cs), [DocumentAssemblerDocxFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerDocxFeaturesTests.cs)를 추가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_planning2\\ -p:IntermediateOutputPath=obj\\verify_doc_planning2\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentPlannerWorkbookScaffoldTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|ExcelSkillExecutiveSummaryLinkTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_planning_tests3\\ -p:IntermediateOutputPath=obj\\verify_doc_planning_tests3\\` 통과 7
|
||||
|
||||
업데이트: 2026-04-14 22:28 (KST)
|
||||
- 문서 포맷 고도화 3차를 반영했습니다. [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)는 DOCX 조립 시 `template_path`와 `page_numbers`를 지원해 사내 템플릿 복제 후 커버, 목차, 머리글, 바닥글, 페이지 번호를 함께 적용할 수 있게 했습니다.
|
||||
- 같은 도구의 DOCX 경로는 템플릿 상속 여부를 [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)의 구조화 문서 리뷰 입력으로 전달해 템플릿 기반 산출물 강점까지 품질 요약에 반영합니다.
|
||||
- [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `data_validations`를 지원하도록 확장했습니다. 단일 시트, summary sheet 포함 워크북, 멀티 시트 워크북 모두에서 OpenXML `DataValidation` 규칙을 생성하고 워크북 품질 리뷰에 검증 규칙 수를 포함합니다.
|
||||
- [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs)는 `decision_summary`, `evidence_cards` 섹션을 지원해 경영 보고형 HTML에서 의사결정 요약과 근거 카드 묶음을 구조화 블록으로 렌더링합니다.
|
||||
- [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)의 HTML 리뷰는 새 블록을 인식해 comparison, roadmap, matrix 외에 decision/evidence 구조도 강점으로 점수화하도록 보강했습니다.
|
||||
- 테스트로 [ExcelSkillDataValidationTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDataValidationTests.cs)를 추가했고, [DocumentAssemblerDocxFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerDocxFeaturesTests.cs), [HtmlSkillConsultingSectionsTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs)를 확장해 DOCX 템플릿/페이지 번호와 HTML decision/evidence 블록을 회귀 검증했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase_next\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerWorkbookScaffoldTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillSummarySheetTests|ExcelSkillDataValidationTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests|DocumentPlannerBusinessDocumentTests" -p:OutputPath=bin\\verify_doc_phase_next_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase_next_tests\\` 통과 9
|
||||
|
||||
@@ -15,9 +15,21 @@ public class DocumentAssemblerDocxFeaturesTests
|
||||
{
|
||||
var workDir = Path.Combine(Path.GetTempPath(), "ax-doc-assemble-features-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(workDir);
|
||||
var templatePath = Path.Combine(workDir, "doc-template.docx");
|
||||
|
||||
try
|
||||
{
|
||||
using (var template = WordprocessingDocument.Create(templatePath, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
|
||||
{
|
||||
var mainPart = template.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body(
|
||||
new Paragraph(new Run(new Text("Template Placeholder"))),
|
||||
new SectionProperties(
|
||||
new PageSize { Width = 15840U, Height = 12240U },
|
||||
new PageMargin { Top = 1080, Right = 1080, Bottom = 1080, Left = 1080 })));
|
||||
mainPart.Document.Save();
|
||||
}
|
||||
|
||||
var tool = new DocumentAssemblerTool();
|
||||
var context = new AgentContext
|
||||
{
|
||||
@@ -34,8 +46,10 @@ public class DocumentAssemblerDocxFeaturesTests
|
||||
"format": "docx",
|
||||
"toc": true,
|
||||
"cover_subtitle": "Q2 Steering Committee",
|
||||
"template_path": "doc-template.docx",
|
||||
"header": "Internal Use Only",
|
||||
"footer": "AX Copilot {page}",
|
||||
"page_numbers": true,
|
||||
"sections": [
|
||||
{ "heading": "1. Executive Summary", "level": 1, "content": "<p>Summary.</p>" },
|
||||
{ "heading": "2. Current State", "level": 1, "content": "<ul><li>Finding A</li><li>Finding B</li></ul>" },
|
||||
@@ -59,6 +73,9 @@ public class DocumentAssemblerDocxFeaturesTests
|
||||
doc.MainDocumentPart.FooterParts.Should().NotBeEmpty();
|
||||
doc.MainDocumentPart.Document.Body!.InnerText.Should().Contain("Q2 Steering Committee");
|
||||
doc.MainDocumentPart.Document.Body.Descendants<FieldCode>().Should().Contain(field => field.Text.Contains("TOC"));
|
||||
doc.MainDocumentPart.FooterParts
|
||||
.SelectMany(part => part.Footer.Descendants<FieldCode>())
|
||||
.Should().Contain(field => field.Text.Contains("PAGE"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class ExcelSkillDataValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WithDataValidations_ShouldPersistValidationRules()
|
||||
{
|
||||
var workDir = Path.Combine(Path.GetTempPath(), "ax-xlsx-validation-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(workDir);
|
||||
|
||||
try
|
||||
{
|
||||
var tool = new ExcelSkill();
|
||||
var context = new AgentContext
|
||||
{
|
||||
WorkFolder = workDir,
|
||||
Permission = "Auto",
|
||||
OperationMode = "external",
|
||||
};
|
||||
|
||||
var args = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"path": "validated.xlsx",
|
||||
"sheet_name": "Tracker",
|
||||
"headers": ["Item", "Status", "Owner"],
|
||||
"rows": [
|
||||
["Task 1", "Open", "Kim"],
|
||||
["Task 2", "Done", "Lee"]
|
||||
],
|
||||
"data_validations": [
|
||||
{
|
||||
"range": "B2:B100",
|
||||
"type": "list",
|
||||
"formula1": "\"Open,In Progress,Done\"",
|
||||
"allow_blank": false,
|
||||
"prompt": "Select a valid status"
|
||||
}
|
||||
]
|
||||
}
|
||||
""").RootElement;
|
||||
|
||||
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Output.Should().Contain("점수");
|
||||
|
||||
var outputPath = Path.Combine(workDir, "validated.xlsx");
|
||||
File.Exists(outputPath).Should().BeTrue();
|
||||
|
||||
using var doc = SpreadsheetDocument.Open(outputPath, false);
|
||||
var firstSheet = doc.WorkbookPart!.Workbook.Sheets!.Elements<Sheet>().First();
|
||||
var worksheetPart = (WorksheetPart)doc.WorkbookPart.GetPartById(firstSheet.Id!);
|
||||
worksheetPart.Worksheet.Elements<DataValidations>().Should().ContainSingle();
|
||||
worksheetPart.Worksheet.Descendants<DataValidation>().Should().ContainSingle();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(workDir))
|
||||
Directory.Delete(workDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,29 +28,44 @@ public class HtmlSkillConsultingSectionsTests
|
||||
"""
|
||||
{
|
||||
"path": "consulting.html",
|
||||
"title": "전략 리뷰",
|
||||
"title": "Executive Report",
|
||||
"body": "",
|
||||
"sections": [
|
||||
{
|
||||
"type": "comparison",
|
||||
"title": "옵션 비교",
|
||||
"title": "Option Comparison",
|
||||
"items": [
|
||||
{ "name": "Option A", "summary": "빠른 실행", "pros": "도입이 쉽다", "cons": "확장성 제약", "verdict": "Fast" }
|
||||
{ "name": "Option A", "summary": "Fast rollout", "pros": "Low setup effort", "cons": "Limited scale", "verdict": "Fast" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "roadmap",
|
||||
"title": "실행 로드맵",
|
||||
"title": "Delivery Roadmap",
|
||||
"phases": [
|
||||
{ "title": "Phase 1", "detail": "진단 및 설계", "timeline": "0-30일", "owner": "PMO" }
|
||||
{ "title": "Phase 1", "detail": "Assess and design", "timeline": "0-30 days", "owner": "PMO" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "matrix",
|
||||
"title": "우선순위 매트릭스",
|
||||
"title": "Priority Matrix",
|
||||
"quadrants": [
|
||||
{ "title": "Quick Wins", "items": ["자동화 과제", "리포트 표준화"] },
|
||||
{ "title": "Strategic Bets", "items": ["플랫폼 재구성"] }
|
||||
{ "title": "Quick Wins", "items": ["Automate reporting", "Standardize templates"] },
|
||||
{ "title": "Strategic Bets", "items": ["Operating model redesign"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "decision_summary",
|
||||
"title": "Decision",
|
||||
"decision": "Approve pilot expansion",
|
||||
"rationale": "Retention and margin improved in the pilot region.",
|
||||
"actions": ["Approve budget", "Launch phase 2"]
|
||||
},
|
||||
{
|
||||
"type": "evidence_cards",
|
||||
"title": "Evidence",
|
||||
"items": [
|
||||
{ "title": "Margin uplift", "detail": "+4.2p improvement after workflow change", "source": "Finance close", "tag": "KPI" },
|
||||
{ "title": "Cycle time", "detail": "Lead time reduced from 12 to 8 days", "source": "PMO tracker", "tag": "Ops" }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -64,6 +79,8 @@ public class HtmlSkillConsultingSectionsTests
|
||||
html.Should().Contain("comparison-grid");
|
||||
html.Should().Contain("roadmap-block");
|
||||
html.Should().Contain("matrix-grid");
|
||||
html.Should().Contain("decision-summary");
|
||||
html.Should().Contain("evidence-cards");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -85,6 +85,8 @@ public static class ArtifactQualityReviewService
|
||||
var comparisonCount = Regex.Matches(html, @"comparison-grid|comparison-card", RegexOptions.IgnoreCase).Count;
|
||||
var roadmapCount = Regex.Matches(html, @"roadmap(-block|-phase)?", RegexOptions.IgnoreCase).Count;
|
||||
var matrixCount = Regex.Matches(html, @"matrix-grid|risk-matrix", RegexOptions.IgnoreCase).Count;
|
||||
var decisionCount = Regex.Matches(html, @"decision-summary", RegexOptions.IgnoreCase).Count;
|
||||
var evidenceCardCount = Regex.Matches(html, @"evidence-cards|evidence-card", RegexOptions.IgnoreCase).Count;
|
||||
var kpiCount = Regex.Matches(html, @"kpi-card|kpi-grid|metric-card", RegexOptions.IgnoreCase).Count;
|
||||
var placeholderCount = CountPlaceholders(html);
|
||||
|
||||
@@ -92,7 +94,7 @@ public static class ArtifactQualityReviewService
|
||||
if (hasTableOfContents) strengths.Add("목차 자동 생성");
|
||||
if (printReady) strengths.Add("인쇄 최적화 CSS 포함");
|
||||
if (sectionCount >= 5) strengths.Add($"핵심 섹션 {sectionCount}개 구성");
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || kpiCount > 0)
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || decisionCount > 0 || evidenceCardCount > 0 || kpiCount > 0)
|
||||
strengths.Add("표/비교/로드맵/KPI 등 구조화 블록 활용");
|
||||
if (calloutCount > 0) strengths.Add("핵심 메시지 강조 블록 포함");
|
||||
|
||||
@@ -102,7 +104,7 @@ public static class ArtifactQualityReviewService
|
||||
issues.Add(new("업무 문서 기준 핵심 섹션 수가 부족합니다", ArtifactReviewSeverity.Warning));
|
||||
if (paragraphCount < sectionCount * 2)
|
||||
issues.Add(new("일부 섹션의 서술이 충분하지 않습니다", ArtifactReviewSeverity.Warning));
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + kpiCount == 0)
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + decisionCount + evidenceCardCount + kpiCount == 0)
|
||||
issues.Add(new("구조화 시각 요소가 없어 보고서 완성도가 낮을 수 있습니다", ArtifactReviewSeverity.Warning));
|
||||
if (placeholderCount > 0)
|
||||
issues.Add(new($"플레이스홀더/미완성 표현 {placeholderCount}건이 남아 있습니다", ArtifactReviewSeverity.Critical));
|
||||
|
||||
@@ -57,8 +57,10 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
},
|
||||
["toc"] = new() { Type = "boolean", Description = "Auto-generate table of contents. Default: true" },
|
||||
["cover_subtitle"] = new() { Type = "string", Description = "Subtitle for cover page. If provided, a cover page is added." },
|
||||
["template_path"] = new() { Type = "string", Description = "Optional .docx template path for DOCX output. Reuses template styles and section setup." },
|
||||
["header"] = new() { Type = "string", Description = "Header text for DOCX output." },
|
||||
["footer"] = new() { Type = "string", Description = "Footer text for DOCX output. Use {page} for page number." },
|
||||
["page_numbers"] = new() { Type = "boolean", Description = "Show page numbers in DOCX footer. Default: true when header or footer is set." },
|
||||
},
|
||||
Required = ["path", "title", "sections"]
|
||||
};
|
||||
@@ -71,8 +73,12 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
var requestedMood = args.SafeTryGetProperty("mood", out var m) ? m.SafeGetString() ?? GetDefaultMood() : GetDefaultMood();
|
||||
var useToc = !args.SafeTryGetProperty("toc", out var tocVal) || tocVal.GetBoolean(); // default true
|
||||
var coverSubtitle = args.SafeTryGetProperty("cover_subtitle", out var cs) ? cs.SafeGetString() : null;
|
||||
var templatePath = args.SafeTryGetProperty("template_path", out var templateEl) ? templateEl.SafeGetString() : null;
|
||||
var headerText = args.SafeTryGetProperty("header", out var hdr) ? hdr.SafeGetString() : null;
|
||||
var footerText = args.SafeTryGetProperty("footer", out var ftr) ? ftr.SafeGetString() : null;
|
||||
var showPageNumbers = args.SafeTryGetProperty("page_numbers", out var pageNumbersEl)
|
||||
? pageNumbersEl.ValueKind == JsonValueKind.True
|
||||
: !string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText);
|
||||
|
||||
if (!args.SafeTryGetProperty("sections", out var sectionsEl) || sectionsEl.ValueKind != JsonValueKind.Array)
|
||||
return ToolResult.Fail("sections 배열이 필요합니다.");
|
||||
@@ -114,13 +120,27 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
var dir = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
string? templateFullPath = null;
|
||||
if (!string.IsNullOrWhiteSpace(templatePath))
|
||||
{
|
||||
templateFullPath = FileReadTool.ResolvePath(templatePath, context.WorkFolder);
|
||||
if (!templateFullPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
|
||||
templateFullPath += ".docx";
|
||||
if (!context.IsPathAllowed(templateFullPath))
|
||||
return ToolResult.Fail($"템플릿 경로 접근 차단: {templateFullPath}");
|
||||
if (!File.Exists(templateFullPath))
|
||||
return ToolResult.Fail($"DOCX 템플릿을 찾을 수 없습니다: {templateFullPath}");
|
||||
if (string.Equals(Path.GetFullPath(templateFullPath), Path.GetFullPath(fullPath), StringComparison.OrdinalIgnoreCase))
|
||||
return ToolResult.Fail("template_path와 path는 같은 파일일 수 없습니다.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string resultMsg;
|
||||
switch (format)
|
||||
{
|
||||
case "docx":
|
||||
resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, headerText, footerText);
|
||||
resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, templateFullPath, headerText, footerText, showPageNumbers);
|
||||
break;
|
||||
case "markdown":
|
||||
resultMsg = AssembleMarkdown(fullPath, title, sections);
|
||||
@@ -239,19 +259,21 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
}
|
||||
|
||||
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections,
|
||||
bool useToc, string? coverSubtitle, string? headerText, string? footerText)
|
||||
bool useToc, string? coverSubtitle, string? templatePath, string? headerText, string? footerText, bool showPageNumbers)
|
||||
{
|
||||
using var doc = DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(
|
||||
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||
var templateApplied = !string.IsNullOrWhiteSpace(templatePath);
|
||||
if (templateApplied)
|
||||
File.Copy(templatePath!, path, true);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
using var doc = templateApplied
|
||||
? DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Open(path, true)
|
||||
: DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||
|
||||
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart();
|
||||
|
||||
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
|
||||
EnsureAssemblerStyles(mainPart, templateApplied);
|
||||
|
||||
var body = InitializeAssemblerBody(mainPart, templateApplied);
|
||||
|
||||
DocumentFormat.OpenXml.Wordprocessing.RunFonts KoreanFonts() => new()
|
||||
{
|
||||
@@ -470,8 +492,8 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
Header = 720, Footer = 720, Gutter = 0 }
|
||||
));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText))
|
||||
AddAssemblerHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers: true);
|
||||
if (!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText) || showPageNumbers)
|
||||
AddAssemblerHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers);
|
||||
|
||||
mainPart.Document.Save();
|
||||
|
||||
@@ -486,7 +508,7 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
0,
|
||||
includeCover,
|
||||
useToc && sections.Count > 3,
|
||||
false,
|
||||
templateApplied,
|
||||
!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText),
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary", "요약")),
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "proposal", "next step", "action", "권고", "실행")),
|
||||
@@ -608,6 +630,45 @@ public class DocumentAssemblerTool : IAgentTool
|
||||
return styles;
|
||||
}
|
||||
|
||||
private static DocumentFormat.OpenXml.Wordprocessing.Body InitializeAssemblerBody(
|
||||
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
|
||||
bool preserveSectionSetup)
|
||||
{
|
||||
mainPart.Document ??= new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||
|
||||
DocumentFormat.OpenXml.Wordprocessing.SectionProperties? preservedSection = null;
|
||||
if (preserveSectionSetup)
|
||||
preservedSection = mainPart.Document.Body?.Elements<DocumentFormat.OpenXml.Wordprocessing.SectionProperties>().LastOrDefault()?.CloneNode(true) as DocumentFormat.OpenXml.Wordprocessing.SectionProperties;
|
||||
|
||||
mainPart.Document.Body = new DocumentFormat.OpenXml.Wordprocessing.Body();
|
||||
var body = mainPart.Document.Body;
|
||||
|
||||
if (preservedSection != null)
|
||||
body.Append(preservedSection);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private static void EnsureAssemblerStyles(
|
||||
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
|
||||
bool preserveExisting)
|
||||
{
|
||||
var stylesPart = mainPart.StyleDefinitionsPart;
|
||||
if (stylesPart == null)
|
||||
{
|
||||
stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!preserveExisting || stylesPart.Styles == null)
|
||||
{
|
||||
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||
stylesPart.Styles.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle)
|
||||
{
|
||||
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||
|
||||
@@ -42,8 +42,9 @@ public class ExcelSkill : IAgentTool
|
||||
"Example: {\"name\":\"Summary\",\"title\":\"2026 운영 리뷰\",\"subtitle\":\"핵심 KPI와 후속 과제\",\"kpis\":[{\"label\":\"Revenue\",\"value\":\"12%\",\"trend\":\"YoY\",\"note\":\"Strong\"}],\"highlights\":[\"핵심 인사이트 1\",\"핵심 인사이트 2\"],\"actions\":[\"즉시 과제 1\",\"즉시 과제 2\"]}"
|
||||
},
|
||||
["number_formats"] = new() { Type = "array", Description = "Number format per column index. Supported: 'currency' (#,##0\"원\"), 'percent' (0.00%), 'decimal' (#,##0.00), 'integer' (#,##0), 'date' (yyyy-mm-dd), or any custom Excel format string. e.g. [\"text\",\"integer\",\"currency\",\"percent\"]", Items = new() { Type = "string" } },
|
||||
["data_validations"] = new() { Type = "array", Description = "Validation rules such as [{\"range\":\"E2:E100\",\"type\":\"list\",\"formula1\":\"\\\"Open,In Progress,Done\\\"\",\"allow_blank\":true,\"prompt\":\"Select status\"}].", Items = new() { Type = "object" } },
|
||||
["col_alignments"] = new() { Type = "array", Description = "Horizontal alignment per column: 'left', 'center', 'right'. Headers always center-aligned.", Items = new() { Type = "string" } },
|
||||
["sheets"] = new() { Type = "array", Description = "Multi-sheet mode: array of sheet objects [{name, headers, rows, style?, theme?, col_widths?, freeze_header?, number_formats?, col_alignments?, merges?, summary_row?}]. When present, overrides top-level headers/rows/sheet_name.", Items = new() { Type = "object" } },
|
||||
["sheets"] = new() { Type = "array", Description = "Multi-sheet mode: array of sheet objects [{name, headers, rows, style?, theme?, col_widths?, freeze_header?, number_formats?, col_alignments?, merges?, summary_row?, data_validations?}]. When present, overrides top-level headers/rows/sheet_name.", Items = new() { Type = "object" } },
|
||||
},
|
||||
Required = []
|
||||
};
|
||||
@@ -183,10 +184,11 @@ public class ExcelSkill : IAgentTool
|
||||
|
||||
var summaryArg = args.SafeTryGetProperty("summary_row", out var sumEl) ? sumEl : default;
|
||||
var mergesArg = args.SafeTryGetProperty("merges", out var mergeEl) ? mergeEl : default;
|
||||
var validationsArg = args.SafeTryGetProperty("data_validations", out var validationsEl) ? validationsEl : default;
|
||||
|
||||
var rowCount = WriteSheetContent(worksheetPart, args, sheetName, headers, rows,
|
||||
var (rowCount, validationCount) = WriteSheetContent(worksheetPart, args, sheetName, headers, rows,
|
||||
isStyled, freezeHeader, theme, numFmts, alignments, customFmts,
|
||||
summaryArg, mergesArg, colCount);
|
||||
summaryArg, mergesArg, validationsArg, colCount);
|
||||
|
||||
var wbSheets = workbookPart.Workbook.AppendChild(new Sheets());
|
||||
wbSheets.Append(new Sheet
|
||||
@@ -209,7 +211,7 @@ public class ExcelSkill : IAgentTool
|
||||
rowCount,
|
||||
CountFormulaCells(rows),
|
||||
0,
|
||||
0,
|
||||
validationCount,
|
||||
false,
|
||||
false,
|
||||
summaryArg.ValueKind == JsonValueKind.Object));
|
||||
@@ -270,9 +272,10 @@ public class ExcelSkill : IAgentTool
|
||||
var colCount = headers.GetArrayLength();
|
||||
var summaryArg = args.SafeTryGetProperty("summary_row", out var sumEl) ? sumEl : default;
|
||||
var mergesArg = args.SafeTryGetProperty("merges", out var mergeEl) ? mergeEl : default;
|
||||
var rowCount = WriteSheetContent(worksheetPart, args, sheetName, headers, rows,
|
||||
var validationsArg = args.SafeTryGetProperty("data_validations", out var validationsEl) ? validationsEl : default;
|
||||
var (rowCount, validationCount) = WriteSheetContent(worksheetPart, args, sheetName, headers, rows,
|
||||
isStyled, freezeHeader, theme, numFmts, alignments, customFmts,
|
||||
summaryArg, mergesArg, colCount);
|
||||
summaryArg, mergesArg, validationsArg, colCount);
|
||||
|
||||
wbSheets.Append(new Sheet
|
||||
{
|
||||
@@ -294,7 +297,7 @@ public class ExcelSkill : IAgentTool
|
||||
rowCount,
|
||||
CountFormulaCells(rows),
|
||||
1,
|
||||
0,
|
||||
validationCount,
|
||||
true,
|
||||
HasSummaryItems(summarySheet, "highlights"),
|
||||
HasSummaryItems(summarySheet, "actions")));
|
||||
@@ -339,6 +342,7 @@ public class ExcelSkill : IAgentTool
|
||||
var totalSheets = 0;
|
||||
var totalRows = 0;
|
||||
var totalFormulaCount = 0;
|
||||
var totalValidationCount = 0;
|
||||
var detailSheetNames = new List<string>();
|
||||
|
||||
foreach (var sheetDef in sheetsArr.EnumerateArray())
|
||||
@@ -381,13 +385,14 @@ public class ExcelSkill : IAgentTool
|
||||
var colCount = headers.GetArrayLength();
|
||||
var summaryArg = sheetDef.SafeTryGetProperty("summary_row", out var sumEl) ? sumEl : default;
|
||||
var mergesArg = sheetDef.SafeTryGetProperty("merges", out var mergeEl) ? mergeEl : default;
|
||||
var validationsArg = sheetDef.SafeTryGetProperty("data_validations", out var validationsEl) ? validationsEl : default;
|
||||
|
||||
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
|
||||
worksheetPart.Worksheet = new Worksheet();
|
||||
|
||||
var rowCount = WriteSheetContent(worksheetPart, sheetDef, sheetName, headers, rows,
|
||||
var (rowCount, validationCount) = WriteSheetContent(worksheetPart, sheetDef, sheetName, headers, rows,
|
||||
isStyled, freezeHeader, theme, numFmts, alignments, combinedCustomFmts,
|
||||
summaryArg, mergesArg, colCount);
|
||||
summaryArg, mergesArg, validationsArg, colCount);
|
||||
|
||||
wbSheets.Append(new Sheet
|
||||
{
|
||||
@@ -400,11 +405,23 @@ public class ExcelSkill : IAgentTool
|
||||
totalSheets++;
|
||||
totalRows += rowCount;
|
||||
totalFormulaCount += CountFormulaCells(rows);
|
||||
totalValidationCount += validationCount;
|
||||
}
|
||||
|
||||
workbookPart.Workbook.Save();
|
||||
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
|
||||
fullPath,
|
||||
totalSheets + (summarySheet.ValueKind == JsonValueKind.Object ? 1 : 0),
|
||||
totalSheets,
|
||||
totalRows,
|
||||
totalFormulaCount,
|
||||
summarySheet.ValueKind == JsonValueKind.Object ? detailSheetNames.Count : 0,
|
||||
totalValidationCount,
|
||||
summarySheet.ValueKind == JsonValueKind.Object,
|
||||
HasSummaryItems(summarySheet, "highlights"),
|
||||
HasSummaryItems(summarySheet, "actions")));
|
||||
return ToolResult.Ok(
|
||||
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}",
|
||||
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}\n{review.ToToolSummary()}",
|
||||
fullPath);
|
||||
}
|
||||
|
||||
@@ -534,7 +551,7 @@ public class ExcelSkill : IAgentTool
|
||||
};
|
||||
}
|
||||
|
||||
private static int WriteSheetContent(
|
||||
private static (int RowCount, int ValidationCount) WriteSheetContent(
|
||||
WorksheetPart worksheetPart,
|
||||
JsonElement args,
|
||||
string sheetName,
|
||||
@@ -548,6 +565,7 @@ public class ExcelSkill : IAgentTool
|
||||
List<(string code, uint id)> customFmtRegistry,
|
||||
JsonElement summaryArg,
|
||||
JsonElement mergesArg,
|
||||
JsonElement validationsArg,
|
||||
int colCount)
|
||||
{
|
||||
// Column widths
|
||||
@@ -626,19 +644,22 @@ public class ExcelSkill : IAgentTool
|
||||
AddSummaryRow(sheetData, summaryArg, rowNum, colCount, rowCount, isStyled);
|
||||
|
||||
// Cell merges
|
||||
MergeCells? mergeCellsSection = null;
|
||||
if (mergesArg.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var mergeCells = new MergeCells();
|
||||
mergeCellsSection = new MergeCells();
|
||||
foreach (var merge in mergesArg.EnumerateArray())
|
||||
{
|
||||
var range = merge.SafeGetString();
|
||||
if (!string.IsNullOrEmpty(range))
|
||||
mergeCells.Append(new MergeCell { Reference = range });
|
||||
mergeCellsSection.Append(new MergeCell { Reference = range });
|
||||
}
|
||||
if (mergeCells.HasChildren)
|
||||
worksheetPart.Worksheet.InsertAfter(mergeCells, sheetData);
|
||||
if (mergeCellsSection.HasChildren)
|
||||
worksheetPart.Worksheet.InsertAfter(mergeCellsSection, sheetData);
|
||||
}
|
||||
|
||||
var validationCount = AddDataValidations(worksheetPart, sheetData, mergeCellsSection, validationsArg);
|
||||
|
||||
// Freeze header
|
||||
if (freezeHeader)
|
||||
{
|
||||
@@ -663,7 +684,64 @@ public class ExcelSkill : IAgentTool
|
||||
worksheetPart.Worksheet.InsertBefore(sheetViews, insertBefore);
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
return (rowCount, validationCount);
|
||||
}
|
||||
|
||||
private static int AddDataValidations(WorksheetPart worksheetPart, SheetData sheetData, MergeCells? mergeCells, JsonElement validationsArg)
|
||||
{
|
||||
if (validationsArg.ValueKind != JsonValueKind.Array || validationsArg.GetArrayLength() == 0)
|
||||
return 0;
|
||||
|
||||
var dataValidations = new DataValidations();
|
||||
var count = 0;
|
||||
|
||||
foreach (var rule in validationsArg.EnumerateArray())
|
||||
{
|
||||
var range = rule.SafeTryGetProperty("range", out var rangeEl) ? rangeEl.SafeGetString() : null;
|
||||
var typeText = rule.SafeTryGetProperty("type", out var typeEl) ? typeEl.SafeGetString() : null;
|
||||
var formula1 = rule.SafeTryGetProperty("formula1", out var formulaEl) ? formulaEl.SafeGetString() : null;
|
||||
if (string.IsNullOrWhiteSpace(range) || string.IsNullOrWhiteSpace(typeText) || string.IsNullOrWhiteSpace(formula1))
|
||||
continue;
|
||||
|
||||
var validation = new DataValidation
|
||||
{
|
||||
Type = ResolveValidationType(typeText),
|
||||
AllowBlank = !(rule.SafeTryGetProperty("allow_blank", out var allowBlankEl) && allowBlankEl.ValueKind == JsonValueKind.False),
|
||||
ShowInputMessage = rule.SafeTryGetProperty("prompt", out _),
|
||||
SequenceOfReferences = new ListValue<StringValue> { InnerText = range }
|
||||
};
|
||||
|
||||
if (rule.SafeTryGetProperty("prompt", out var promptEl) && !string.IsNullOrWhiteSpace(promptEl.SafeGetString()))
|
||||
{
|
||||
validation.PromptTitle = "Input";
|
||||
validation.Prompt = promptEl.SafeGetString();
|
||||
}
|
||||
|
||||
validation.Append(new Formula1(formula1));
|
||||
dataValidations.Append(validation);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
return 0;
|
||||
|
||||
dataValidations.Count = (uint)count;
|
||||
var anchor = (OpenXmlElement?)mergeCells ?? sheetData;
|
||||
worksheetPart.Worksheet.InsertAfter(dataValidations, anchor);
|
||||
return count;
|
||||
}
|
||||
|
||||
private static DataValidationValues ResolveValidationType(string? type)
|
||||
{
|
||||
return (type ?? "list").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"whole" or "whole_number" => DataValidationValues.Whole,
|
||||
"decimal" => DataValidationValues.Decimal,
|
||||
"date" => DataValidationValues.Date,
|
||||
"text_length" => DataValidationValues.TextLength,
|
||||
"custom" => DataValidationValues.Custom,
|
||||
_ => DataValidationValues.List,
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
@@ -52,6 +52,8 @@ public class HtmlSkill : IAgentTool
|
||||
"'table' {headers:[], rows:[[]]}, " +
|
||||
"'chart' {kind:'bar|horizontal_bar', title, data:[{label, value, color}]}, " +
|
||||
"'cards' {items:[{title, body, badge, icon}]}, " +
|
||||
"'decision_summary' {title, decision, rationale, actions:['...']}, " +
|
||||
"'evidence_cards' {title, items:[{title, detail, source, tag}]}, " +
|
||||
"'comparison' {title, items:[{name, summary, pros, cons, verdict}]}, " +
|
||||
"'roadmap' {title, phases:[{title, detail, timeline, owner}]}, " +
|
||||
"'matrix' {title, quadrants:[{title, items:['...']}]}, " +
|
||||
@@ -334,6 +336,12 @@ public class HtmlSkill : IAgentTool
|
||||
case "cards":
|
||||
sb.AppendLine(RenderCards(section));
|
||||
break;
|
||||
case "decision_summary":
|
||||
sb.AppendLine(RenderDecisionSummary(section));
|
||||
break;
|
||||
case "evidence_cards":
|
||||
sb.AppendLine(RenderEvidenceCards(section));
|
||||
break;
|
||||
case "comparison":
|
||||
sb.AppendLine(RenderComparison(section));
|
||||
break;
|
||||
@@ -525,6 +533,67 @@ public class HtmlSkill : IAgentTool
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderDecisionSummary(JsonElement s)
|
||||
{
|
||||
var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : "Decision Summary";
|
||||
var decision = s.SafeTryGetProperty("decision", out var decisionEl) ? decisionEl.SafeGetString() : null;
|
||||
var rationale = s.SafeTryGetProperty("rationale", out var rationaleEl) ? rationaleEl.SafeGetString() : null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<section class=\"decision-summary\" style=\"margin:1.4rem 0;padding:1.2rem 1.3rem;border:1px solid #dbe4f0;border-radius:16px;background:linear-gradient(180deg,#f8fbff,#ffffff);box-shadow:0 10px 28px rgba(15,23,42,.06);\">");
|
||||
sb.AppendLine($"<div style=\"font-size:1rem;font-weight:800;color:#0f172a;margin-bottom:.55rem\">{Escape(title ?? "Decision Summary")}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(decision))
|
||||
sb.AppendLine($"<div style=\"padding:.8rem 1rem;border-radius:12px;background:#eef2ff;color:#312e81;font-weight:700;margin-bottom:.9rem\">Decision: {MarkdownToHtml(decision)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(rationale))
|
||||
sb.AppendLine($"<div style=\"color:#334155;line-height:1.7;margin-bottom:.8rem\">{MarkdownToHtml(rationale)}</div>");
|
||||
|
||||
if (s.SafeTryGetProperty("actions", out var actionsEl) && actionsEl.ValueKind == JsonValueKind.Array && actionsEl.GetArrayLength() > 0)
|
||||
{
|
||||
sb.AppendLine("<div style=\"font-size:.86rem;font-weight:700;color:#475569;margin-bottom:.35rem\">Immediate Actions</div>");
|
||||
sb.AppendLine("<ul style=\"margin:0;padding-left:1.2rem;color:#334155;line-height:1.7;\">");
|
||||
foreach (var item in actionsEl.EnumerateArray())
|
||||
sb.AppendLine($"<li>{MarkdownToHtml(item.SafeGetString() ?? string.Empty)}</li>");
|
||||
sb.AppendLine("</ul>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</section>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderEvidenceCards(JsonElement s)
|
||||
{
|
||||
if (!s.SafeTryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array)
|
||||
return string.Empty;
|
||||
|
||||
var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : null;
|
||||
var sb = new StringBuilder();
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
sb.AppendLine($"<h3>{Escape(title)}</h3>");
|
||||
|
||||
sb.AppendLine("<div class=\"evidence-cards\" style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem;margin:1.2rem 0;\">");
|
||||
foreach (var item in items.EnumerateArray())
|
||||
{
|
||||
var cardTitle = item.SafeTryGetProperty("title", out var cardTitleEl) ? cardTitleEl.SafeGetString() : null;
|
||||
var detail = item.SafeTryGetProperty("detail", out var detailEl) ? detailEl.SafeGetString() : null;
|
||||
var source = item.SafeTryGetProperty("source", out var sourceEl) ? sourceEl.SafeGetString() : null;
|
||||
var tag = item.SafeTryGetProperty("tag", out var tagEl) ? tagEl.SafeGetString() : null;
|
||||
|
||||
sb.AppendLine("<article style=\"border:1px solid #e2e8f0;border-radius:14px;padding:1rem 1.05rem;background:#fff;box-shadow:0 8px 22px rgba(15,23,42,.05);\">");
|
||||
sb.Append("<div style=\"display:flex;align-items:center;gap:.5rem;margin-bottom:.45rem;\">");
|
||||
sb.Append($"<strong style=\"font-size:.95rem;color:#0f172a\">{Escape(cardTitle ?? "Evidence")}</strong>");
|
||||
if (!string.IsNullOrWhiteSpace(tag))
|
||||
sb.Append($"<span style=\"margin-left:auto;font-size:.72rem;font-weight:700;padding:.2rem .45rem;border-radius:999px;background:#eff6ff;color:#2563eb\">{Escape(tag)}</span>");
|
||||
sb.AppendLine("</div>");
|
||||
if (!string.IsNullOrWhiteSpace(detail))
|
||||
sb.AppendLine($"<div style=\"color:#334155;line-height:1.65\">{MarkdownToHtml(detail)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(source))
|
||||
sb.AppendLine($"<div style=\"margin-top:.65rem;font-size:.78rem;color:#64748b\">Source: {Escape(source)}</div>");
|
||||
sb.AppendLine("</article>");
|
||||
}
|
||||
sb.AppendLine("</div>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderComparison(JsonElement s)
|
||||
{
|
||||
if (!s.SafeTryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array)
|
||||
|
||||
Reference in New Issue
Block a user