문서 고도화 다음 단계를 반영해 XLSX·HTML·PPT 품질 게이트를 강화했습니다
핵심 수정사항: - ExcelSkill summary_sheet에 trend_series를 추가해 Trend Dashboard 블록을 렌더링하고, workbook quality review가 dashboard형 summary 구성을 더 정확히 평가하도록 확장했습니다. - HtmlSkill은 print=true인데 print_header/print_footer가 없는 경우 기본 print frame을 자동 생성하도록 보강했고, ArtifactQualityReviewService는 print-ready 문서의 frame/decision/evidence/cover 부족을 추가 경고로 반환합니다. - DeckPlanningService는 comparison, roadmap, executive_summary, kpi_dashboard 슬라이드의 최소 구조를 자동 보정하고, DeckQualityReviewService는 slide-level quality gate를 추가해 긴 headline, 과밀 슬라이드, 옵션 부족, 표/차트 데이터 누락을 Slide N 경고로 요약합니다. - DeckQualityReviewServiceTests, ExcelSkillDashboardSummaryTests, HtmlSkillPrintFrameTests를 확장해 회귀 검증을 강화했습니다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\ : 통과 13
This commit is contained in:
@@ -1830,3 +1830,11 @@ MIT License
|
|||||||
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 추가했고, [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)도 새 workbook review 입력 형식에 맞춰 갱신했습니다.
|
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 추가했고, [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)도 새 workbook review 입력 형식에 맞춰 갱신했습니다.
|
||||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\` 경고 0 / 오류 0
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\` 경고 0 / 오류 0
|
||||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\` 통과 11
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\` 통과 11
|
||||||
|
|
||||||
|
업데이트: 2026-04-14 23:15 (KST)
|
||||||
|
- 문서 고도화 다음 단계를 반영했습니다. [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `summary_sheet.trend_series`를 지원해 `Trend Dashboard` 블록을 추가로 렌더링하고, 기존 `decision_summary`/`scorecards`/`sheet_summaries`와 함께 보고서형 workbook summary 밀도를 높였습니다.
|
||||||
|
- [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs)는 `print=true`인데 `print_header`/`print_footer`가 없는 경우 기본 print frame을 자동 생성하도록 보강했습니다. 같은 흐름에서 [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 print-ready HTML이 frame, decision summary, evidence card 없이 끝나는 경우를 품질 경고로 잡아냅니다.
|
||||||
|
- PPT 쪽은 [DeckPlanningService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckPlanningService.cs)가 `comparison`, `roadmap`, `executive_summary`, `kpi_dashboard` 슬라이드의 최소 구조를 더 적극적으로 자동 보정하고, [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 slide-level quality gate를 추가해 긴 headline, 과밀 슬라이드, 비교 옵션 부족, 차트/표 데이터 누락을 슬라이드 번호와 함께 경고합니다.
|
||||||
|
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [HtmlSkillPrintFrameTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillPrintFrameTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)를 확장해 trend dashboard, default print frame, slide-level alert를 회귀 검증했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\` 통과 13
|
||||||
|
|||||||
@@ -899,3 +899,14 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
|||||||
- 새 회귀 테스트 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 추가했고, [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)도 새 record 시그니처에 맞춰 갱신했습니다.
|
- 새 회귀 테스트 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [DocumentAssemblerStyleMapTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerStyleMapTests.cs)를 추가했고, [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)도 새 record 시그니처에 맞춰 갱신했습니다.
|
||||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\` 경고 0 / 오류 0
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\` 경고 0 / 오류 0
|
||||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\` 통과 11
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\` 통과 11
|
||||||
|
|
||||||
|
업데이트: 2026-04-14 23:15 (KST)
|
||||||
|
- [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)는 `summary_sheet.trend_series`를 새로 지원합니다. summary sheet에서 `Trend Dashboard` 섹션을 추가로 만들고 `label/current/target/delta/status`를 열 기반으로 렌더링해 workbook summary가 KPI 표 수준을 넘어 상태 대시보드 역할까지 하도록 확장했습니다.
|
||||||
|
- 같은 파일의 workbook review 입력 계산은 `trend_series`도 summary quality 강점으로 인정하도록 업데이트했습니다. [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)는 workbook summary가 KPI/decision/highlight 없이 끝나는 경우 보완 포인트를 추가로 반환합니다.
|
||||||
|
- [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs)는 `print=true`에서 명시적 `print_header`/`print_footer`가 없는 경우 기본 frame(`title`, `date | AX Copilot`)을 자동 생성합니다. print-ready HTML이 최소 배포형 header/footer를 갖도록 내부 기본값을 넣은 것입니다.
|
||||||
|
- [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)의 HTML 리뷰는 print-ready 문서에 frame이 없거나, decision/evidence block이 부족하거나, 장문 보고서인데 cover가 없는 경우를 추가로 경고합니다.
|
||||||
|
- [DeckPlanningService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckPlanningService.cs)는 `comparison`, `roadmap`, `executive_summary`, `kpi_dashboard` 슬라이드의 최소 구조를 자동 보정하고, 긴 headline은 내부 기준 길이로 압축합니다.
|
||||||
|
- [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs)는 slide-level quality gate를 추가해 긴 headline, 과밀 슬라이드, 옵션 부족, 표/차트 데이터 누락을 `Slide N:` 경고로 품질 요약에 포함합니다.
|
||||||
|
- 테스트로 [ExcelSkillDashboardSummaryTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillDashboardSummaryTests.cs), [HtmlSkillPrintFrameTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillPrintFrameTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)를 확장했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next3\\ -p:IntermediateOutputPath=obj\\verify_doc_next3\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|ArtifactQualityReviewServiceTests" -p:OutputPath=bin\\verify_doc_next3_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next3_tests\\` 통과 13
|
||||||
|
|||||||
@@ -54,4 +54,33 @@ public class DeckQualityReviewServiceTests
|
|||||||
review.Strengths.Should().Contain(strength => strength.Contains("Executive Summary"));
|
review.Strengths.Should().Contain(strength => strength.Contains("Executive Summary"));
|
||||||
review.Issues.Should().NotContain(issue => issue.Severity == DeckReviewSeverity.Critical);
|
review.Issues.Should().NotContain(issue => issue.Severity == DeckReviewSeverity.Critical);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReviewDeck_ShouldSurfaceSlideLevelQualityAlerts()
|
||||||
|
{
|
||||||
|
using var slides = JsonDocument.Parse(
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{ "layout": "title", "title": "Growth Strategy" },
|
||||||
|
{
|
||||||
|
"layout": "executive_summary",
|
||||||
|
"title": "Executive Summary",
|
||||||
|
"headline": "This headline is intentionally very long so that it exceeds the preferred consulting slide headline length and triggers the slide quality gate.",
|
||||||
|
"summary_points": ["Only one point"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"layout": "comparison",
|
||||||
|
"title": "Options",
|
||||||
|
"headline": "Compare options",
|
||||||
|
"options": [{ "name": "A", "pros": "Fast", "cons": "Shallow", "verdict": "Only option" }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
|
||||||
|
var review = DeckQualityReviewService.ReviewDeck("Alert Deck", slides.RootElement, hasTemplate: false, autoRepairCount: 0);
|
||||||
|
|
||||||
|
review.Issues.Should().Contain(issue => issue.Message.Contains("Slide 2"));
|
||||||
|
review.Issues.Should().Contain(issue => issue.Message.Contains("headline is too long"));
|
||||||
|
review.Issues.Should().Contain(issue => issue.Message.Contains("comparison slide needs at least two options"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ public class ExcelSkillDashboardSummaryTests
|
|||||||
"scorecards": [
|
"scorecards": [
|
||||||
{ "label": "Run-rate", "value": "96%", "status": "On Track", "note": "Ahead of plan" }
|
{ "label": "Run-rate", "value": "96%", "status": "On Track", "note": "Ahead of plan" }
|
||||||
],
|
],
|
||||||
|
"trend_series": [
|
||||||
|
{ "label": "Revenue", "current": "120", "target": "130", "delta": "-10", "status": "Watch" }
|
||||||
|
],
|
||||||
"sheet_summaries": [
|
"sheet_summaries": [
|
||||||
{ "sheet": "Revenue", "status": "Watch", "summary": "SMB margin pressure", "owner": "Sales Ops" }
|
{ "sheet": "Revenue", "status": "Watch", "summary": "SMB margin pressure", "owner": "Sales Ops" }
|
||||||
],
|
],
|
||||||
@@ -73,6 +76,8 @@ public class ExcelSkillDashboardSummaryTests
|
|||||||
texts.Should().Contain("Approve regional rollout");
|
texts.Should().Contain("Approve regional rollout");
|
||||||
texts.Should().Contain("Scorecards");
|
texts.Should().Contain("Scorecards");
|
||||||
texts.Should().Contain("Run-rate");
|
texts.Should().Contain("Run-rate");
|
||||||
|
texts.Should().Contain("Trend Dashboard");
|
||||||
|
texts.Should().Contain("-10");
|
||||||
texts.Should().Contain("Revenue");
|
texts.Should().Contain("Revenue");
|
||||||
texts.Should().Contain("SMB margin pressure");
|
texts.Should().Contain("SMB margin pressure");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,52 @@ public class HtmlSkillPrintFrameTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_WithPrintEnabled_ShouldGenerateDefaultPrintFrame()
|
||||||
|
{
|
||||||
|
var workDir = Path.Combine(Path.GetTempPath(), "ax-html-printframe-auto-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(workDir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tool = new HtmlSkill();
|
||||||
|
var context = new AgentContext
|
||||||
|
{
|
||||||
|
WorkFolder = workDir,
|
||||||
|
Permission = "Auto",
|
||||||
|
OperationMode = "external",
|
||||||
|
};
|
||||||
|
|
||||||
|
var args = JsonDocument.Parse(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"path": "auto-print-frame.html",
|
||||||
|
"title": "Strategy Review",
|
||||||
|
"print": true,
|
||||||
|
"body": "<h2>Executive Summary</h2><p>Summary text with enough detail to qualify as a printable executive report.</p><h2>Decision</h2><p>Approve the phase-1 roadmap.</p><h2>Evidence</h2><p>Supporting evidence.</p><h2>Appendix</h2><p>Reference detail.</p>"
|
||||||
|
}
|
||||||
|
""").RootElement;
|
||||||
|
|
||||||
|
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var html = File.ReadAllText(Path.Combine(workDir, "auto-print-frame.html"));
|
||||||
|
html.Should().Contain("print-header");
|
||||||
|
html.Should().Contain("print-footer");
|
||||||
|
html.Should().Contain("Strategy Review");
|
||||||
|
html.Should().Contain("AX Copilot");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(workDir))
|
||||||
|
Directory.Delete(workDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public sealed record ArtifactQualityReport(
|
|||||||
var parts = new List<string> { $"Quality score {Score}/100" };
|
var parts = new List<string> { $"Quality score {Score}/100" };
|
||||||
if (strengths.Count > 0)
|
if (strengths.Count > 0)
|
||||||
parts.Add("Strengths: " + string.Join(", ", strengths));
|
parts.Add("Strengths: " + string.Join(", ", strengths));
|
||||||
|
var slideAlertCount = Issues.Count(issue => issue.Message.StartsWith("Slide ", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (slideAlertCount > 0)
|
||||||
|
parts.Add($"Slide alerts: {slideAlertCount}");
|
||||||
if (issues.Count > 0)
|
if (issues.Count > 0)
|
||||||
parts.Add("Needs work: " + string.Join(", ", issues));
|
parts.Add("Needs work: " + string.Join(", ", issues));
|
||||||
else
|
else
|
||||||
@@ -148,6 +151,12 @@ public static class ArtifactQualityReviewService
|
|||||||
issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", ArtifactReviewSeverity.Critical));
|
issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", ArtifactReviewSeverity.Critical));
|
||||||
if (!ContainsAny(html, ExecutiveKeywords))
|
if (!ContainsAny(html, ExecutiveKeywords))
|
||||||
issues.Add(new("Executive summary or summary section is missing.", ArtifactReviewSeverity.Warning));
|
issues.Add(new("Executive summary or summary section is missing.", ArtifactReviewSeverity.Warning));
|
||||||
|
if (printReady && !hasPrintFrame)
|
||||||
|
issues.Add(new("Print-ready export is missing a print header/footer frame.", ArtifactReviewSeverity.Info));
|
||||||
|
if (printReady && sectionCount >= 4 && decisionCount + evidenceCardCount == 0)
|
||||||
|
issues.Add(new("Print-ready business report would benefit from decision summary or evidence cards.", ArtifactReviewSeverity.Warning));
|
||||||
|
if (printReady && !hasCover && sectionCount >= 5)
|
||||||
|
issues.Add(new("Print-ready report could benefit from a cover page.", ArtifactReviewSeverity.Info));
|
||||||
|
|
||||||
return BuildReport("html", strengths, issues);
|
return BuildReport("html", strengths, issues);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,21 @@ public static class DeckPlanningService
|
|||||||
repairs++;
|
repairs++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (normalizedLayout == "comparison" && EnsureMinimumOptions(slide))
|
||||||
|
repairs++;
|
||||||
|
|
||||||
|
if (normalizedLayout == "roadmap" && EnsureRoadmapPhases(slide))
|
||||||
|
repairs++;
|
||||||
|
|
||||||
|
if (normalizedLayout == "executive_summary")
|
||||||
|
{
|
||||||
|
repairs += EnsureExecutiveSummaryContent(slide, objective, decisionAsk);
|
||||||
|
repairs += EnsureKpiContent(slide);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedLayout == "kpi_dashboard")
|
||||||
|
repairs += EnsureKpiContent(slide);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ReadString(slide, "headline")) &&
|
if (string.IsNullOrWhiteSpace(ReadString(slide, "headline")) &&
|
||||||
normalizedLayout is "executive_summary" or "comparison" or "recommendation" or "roadmap" or "kpi_dashboard")
|
normalizedLayout is "executive_summary" or "comparison" or "recommendation" or "roadmap" or "kpi_dashboard")
|
||||||
{
|
{
|
||||||
@@ -256,6 +271,9 @@ public static class DeckPlanningService
|
|||||||
repairs++;
|
repairs++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TrimLongHeadline(slide))
|
||||||
|
repairs++;
|
||||||
|
|
||||||
if (normalizedLayout == "recommendation" && string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
|
if (normalizedLayout == "recommendation" && string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
|
||||||
{
|
{
|
||||||
slide["recommendation"] = Coalesce(decisionAsk, objective, "利됱떆 ?ㅽ뻾??沅뚭퀬?덉쓣 ??臾몄옣?쇰줈 ?쒖떆?⑸땲??");
|
slide["recommendation"] = Coalesce(decisionAsk, objective, "利됱떆 ?ㅽ뻾??沅뚭퀬?덉쓣 ??臾몄옣?쇰줈 ?쒖떆?⑸땲??");
|
||||||
@@ -497,6 +515,80 @@ public static class DeckPlanningService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool EnsureMinimumOptions(JsonObject slide)
|
||||||
|
{
|
||||||
|
if (!slide.TryGetPropertyValue("options", out var optionsNode) || optionsNode is not JsonArray options)
|
||||||
|
{
|
||||||
|
EnsureFallbackOptions(slide);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.Count >= 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var existing = options.OfType<JsonObject>().ToList();
|
||||||
|
EnsureFallbackOptions(slide);
|
||||||
|
if (existing.Count > 0 && slide["options"] is JsonArray fallbackOptions)
|
||||||
|
fallbackOptions.Insert(0, existing[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EnsureRoadmapPhases(JsonObject slide)
|
||||||
|
{
|
||||||
|
if (slide.TryGetPropertyValue("phases", out var phasesNode) && phasesNode is JsonArray phases && phases.Count > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slide["phases"] = BuildRoadmapSlide(ReadString(slide, "headline")).TryGetPropertyValue("phases", out var defaultPhases)
|
||||||
|
? defaultPhases?.DeepClone()
|
||||||
|
: new JsonArray();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnsureExecutiveSummaryContent(JsonObject slide, string? objective, string? decisionAsk)
|
||||||
|
{
|
||||||
|
var repairs = 0;
|
||||||
|
if (ReadTextLines(slide, "summary_points").Count == 0)
|
||||||
|
{
|
||||||
|
slide["summary_points"] = new JsonArray(
|
||||||
|
Coalesce(decisionAsk, objective, "Key decision and expected business impact are summarized here."),
|
||||||
|
"Immediate execution priorities are separated from medium-term actions.",
|
||||||
|
"Supporting evidence is translated into a concise executive message.");
|
||||||
|
repairs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
|
||||||
|
{
|
||||||
|
slide["recommendation"] = Coalesce(decisionAsk, objective, "Confirm the top-priority initiative and move to execution.");
|
||||||
|
repairs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return repairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnsureKpiContent(JsonObject slide)
|
||||||
|
{
|
||||||
|
if (slide.TryGetPropertyValue("kpis", out var kpisNode) && kpisNode is JsonArray kpis && kpis.Count > 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
slide["kpis"] = new JsonArray
|
||||||
|
{
|
||||||
|
new JsonObject { ["label"] = "Impact", ["value"] = "High", ["trend"] = "Near term", ["note"] = "Primary KPI" },
|
||||||
|
new JsonObject { ["label"] = "Risk", ["value"] = "Managed", ["trend"] = "Tracked", ["note"] = "Watch item" },
|
||||||
|
new JsonObject { ["label"] = "Owner", ["value"] = "Confirmed", ["trend"] = "Ready", ["note"] = "Execution aligned" }
|
||||||
|
};
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TrimLongHeadline(JsonObject slide)
|
||||||
|
{
|
||||||
|
var headline = ReadString(slide, "headline");
|
||||||
|
if (string.IsNullOrWhiteSpace(headline) || headline.Length <= 88)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slide["headline"] = headline[..88].TrimEnd() + "...";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool HasChartData(JsonObject slide)
|
private static bool HasChartData(JsonObject slide)
|
||||||
{
|
{
|
||||||
return slide.TryGetPropertyValue("chart_labels", out var labelsNode) &&
|
return slide.TryGetPropertyValue("chart_labels", out var labelsNode) &&
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public sealed record DeckQualityReport(
|
|||||||
var parts = new List<string> { $"PPT quality {Score}/100" };
|
var parts = new List<string> { $"PPT quality {Score}/100" };
|
||||||
if (strengths.Count > 0)
|
if (strengths.Count > 0)
|
||||||
parts.Add("Strengths: " + string.Join(", ", strengths));
|
parts.Add("Strengths: " + string.Join(", ", strengths));
|
||||||
|
var slideAlertCount = Issues.Count(issue => issue.Message.StartsWith("Slide ", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (slideAlertCount > 0)
|
||||||
|
parts.Add($"Slide alerts: {slideAlertCount}");
|
||||||
if (issues.Count > 0)
|
if (issues.Count > 0)
|
||||||
parts.Add("Needs work: " + string.Join(", ", issues));
|
parts.Add("Needs work: " + string.Join(", ", issues));
|
||||||
else
|
else
|
||||||
@@ -63,12 +66,14 @@ public static class DeckQualityReviewService
|
|||||||
var tableSlides = 0;
|
var tableSlides = 0;
|
||||||
var comparisonSlides = 0;
|
var comparisonSlides = 0;
|
||||||
var textHeavySlides = 0;
|
var textHeavySlides = 0;
|
||||||
|
var slideAlertCount = 0;
|
||||||
var duplicateHeadlines = 0;
|
var duplicateHeadlines = 0;
|
||||||
var placeholderCount = 0;
|
var placeholderCount = 0;
|
||||||
var headlines = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var headlines = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var layoutCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
var layoutCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var slide in slidesList)
|
for (var slideIndex = 0; slideIndex < slidesList.Count; slideIndex++)
|
||||||
{
|
{
|
||||||
|
var slide = slidesList[slideIndex];
|
||||||
var layout = ReadString(slide, "layout", "content");
|
var layout = ReadString(slide, "layout", "content");
|
||||||
var normalizedLayout = layout.ToLowerInvariant();
|
var normalizedLayout = layout.ToLowerInvariant();
|
||||||
layoutCounts[normalizedLayout] = layoutCounts.GetValueOrDefault(normalizedLayout) + 1;
|
layoutCounts[normalizedLayout] = layoutCounts.GetValueOrDefault(normalizedLayout) + 1;
|
||||||
@@ -118,6 +123,7 @@ public static class DeckQualityReviewService
|
|||||||
ReadString(slide, "right").Length;
|
ReadString(slide, "right").Length;
|
||||||
if (bulletCount >= 7 || textLength >= 420)
|
if (bulletCount >= 7 || textLength >= 420)
|
||||||
textHeavySlides++;
|
textHeavySlides++;
|
||||||
|
slideAlertCount += AddSlideSpecificIssues(issues, slide, slideIndex + 1, normalizedLayout, headline, bulletCount, textLength);
|
||||||
placeholderCount += CountPlaceholders(slide.GetRawText());
|
placeholderCount += CountPlaceholders(slide.GetRawText());
|
||||||
}
|
}
|
||||||
if (hasTemplate) strengths.Add("Uses template or branded theme");
|
if (hasTemplate) strengths.Add("Uses template or branded theme");
|
||||||
@@ -131,6 +137,8 @@ public static class DeckQualityReviewService
|
|||||||
strengths.Add($"Uses {layoutCounts.Count} distinct layouts");
|
strengths.Add($"Uses {layoutCounts.Count} distinct layouts");
|
||||||
if (autoRepairCount > 0)
|
if (autoRepairCount > 0)
|
||||||
strengths.Add($"Auto-repair applied {autoRepairCount} time(s)");
|
strengths.Add($"Auto-repair applied {autoRepairCount} time(s)");
|
||||||
|
if (slideAlertCount == 0)
|
||||||
|
strengths.Add("Slides pass quality gate checks");
|
||||||
if (slideCount < 4)
|
if (slideCount < 4)
|
||||||
issues.Add(new("Slide count may be too low for an executive deck.", DeckReviewSeverity.Warning));
|
issues.Add(new("Slide count may be too low for an executive deck.", DeckReviewSeverity.Warning));
|
||||||
if (titleSlides == 0)
|
if (titleSlides == 0)
|
||||||
@@ -162,6 +170,77 @@ public static class DeckQualityReviewService
|
|||||||
score = Math.Clamp(score, 30, 98);
|
score = Math.Clamp(score, 30, 98);
|
||||||
return new DeckQualityReport(score, strengths, issues, storylineSteps);
|
return new DeckQualityReport(score, strengths, issues, storylineSteps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int AddSlideSpecificIssues(
|
||||||
|
List<DeckReviewIssue> issues,
|
||||||
|
JsonElement slide,
|
||||||
|
int slideNumber,
|
||||||
|
string normalizedLayout,
|
||||||
|
string headline,
|
||||||
|
int bulletCount,
|
||||||
|
int textLength)
|
||||||
|
{
|
||||||
|
var added = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(headline) && headline.Length > 90)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: headline is too long and may blur the core message.", DeckReviewSeverity.Info));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulletCount >= 8 || textLength >= 520)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: content density is high and should be simplified.", DeckReviewSeverity.Warning));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (normalizedLayout)
|
||||||
|
{
|
||||||
|
case "executive_summary":
|
||||||
|
if (ReadStringList(slide, "summary_points").Count < 2)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: Executive Summary needs more supporting points.", DeckReviewSeverity.Warning));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "recommendation":
|
||||||
|
if (string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: recommendation text is missing.", DeckReviewSeverity.Warning));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "comparison":
|
||||||
|
if (ReadJsonArrayCount(slide, "options") < 2)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: comparison slide needs at least two options.", DeckReviewSeverity.Warning));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "roadmap":
|
||||||
|
if (ReadJsonArrayCount(slide, "phases") == 0)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: roadmap slide is missing phases.", DeckReviewSeverity.Warning));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "chart":
|
||||||
|
if (ReadJsonArrayCount(slide, "chart_labels") == 0 || ReadJsonArrayCount(slide, "chart_values") == 0)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: chart slide is missing chart data.", DeckReviewSeverity.Critical));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "table":
|
||||||
|
if (ReadJsonArrayCount(slide, "headers") == 0 || ReadJsonArrayCount(slide, "rows") == 0)
|
||||||
|
{
|
||||||
|
issues.Add(new($"Slide {slideNumber}: table slide is missing headers or rows.", DeckReviewSeverity.Warning));
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
private static string ReadString(JsonElement element, string propertyName, string fallback = "")
|
private static string ReadString(JsonElement element, string propertyName, string fallback = "")
|
||||||
{
|
{
|
||||||
if (!element.SafeTryGetProperty(propertyName, out var value))
|
if (!element.SafeTryGetProperty(propertyName, out var value))
|
||||||
@@ -199,6 +278,13 @@ public static class DeckQualityReviewService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ReadJsonArrayCount(JsonElement element, string propertyName)
|
||||||
|
{
|
||||||
|
return element.SafeTryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.Array
|
||||||
|
? value.GetArrayLength()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static int CountPlaceholders(string text)
|
private static int CountPlaceholders(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ public class ExcelSkill : IAgentTool
|
|||||||
true,
|
true,
|
||||||
HasSummaryItems(summarySheet, "highlights"),
|
HasSummaryItems(summarySheet, "highlights"),
|
||||||
HasSummaryItems(summarySheet, "actions"),
|
HasSummaryItems(summarySheet, "actions"),
|
||||||
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis"),
|
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"),
|
||||||
HasStructuredSummaryContent(summarySheet, "decision_summary"),
|
HasStructuredSummaryContent(summarySheet, "decision_summary"),
|
||||||
HasSummaryItems(summarySheet, "sheet_summaries")));
|
HasSummaryItems(summarySheet, "sheet_summaries")));
|
||||||
features += $"\n{review.ToToolSummary()}";
|
features += $"\n{review.ToToolSummary()}";
|
||||||
@@ -435,7 +435,7 @@ public class ExcelSkill : IAgentTool
|
|||||||
summarySheet.ValueKind == JsonValueKind.Object,
|
summarySheet.ValueKind == JsonValueKind.Object,
|
||||||
HasSummaryItems(summarySheet, "highlights"),
|
HasSummaryItems(summarySheet, "highlights"),
|
||||||
HasSummaryItems(summarySheet, "actions"),
|
HasSummaryItems(summarySheet, "actions"),
|
||||||
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis"),
|
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"),
|
||||||
HasStructuredSummaryContent(summarySheet, "decision_summary"),
|
HasStructuredSummaryContent(summarySheet, "decision_summary"),
|
||||||
HasSummaryItems(summarySheet, "sheet_summaries")));
|
HasSummaryItems(summarySheet, "sheet_summaries")));
|
||||||
return ToolResult.Ok(
|
return ToolResult.Ok(
|
||||||
@@ -482,6 +482,7 @@ public class ExcelSkill : IAgentTool
|
|||||||
|
|
||||||
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
|
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
|
||||||
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
|
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
|
||||||
|
AppendTrendSection(summarySheet, sheetData, ref rowIndex);
|
||||||
|
|
||||||
if (summarySheet.SafeTryGetProperty("kpis", out var kpis) && kpis.ValueKind == JsonValueKind.Array && kpis.GetArrayLength() > 0)
|
if (summarySheet.SafeTryGetProperty("kpis", out var kpis) && kpis.ValueKind == JsonValueKind.Array && kpis.GetArrayLength() > 0)
|
||||||
{
|
{
|
||||||
@@ -652,6 +653,38 @@ public class ExcelSkill : IAgentTool
|
|||||||
rowIndex++;
|
rowIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AppendTrendSection(JsonElement summarySheet, SheetData sheetData, ref uint rowIndex)
|
||||||
|
{
|
||||||
|
if (!summarySheet.SafeTryGetProperty("trend_series", out var trendSeries)
|
||||||
|
|| trendSeries.ValueKind != JsonValueKind.Array
|
||||||
|
|| trendSeries.GetArrayLength() == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var headerRow = new Row { RowIndex = rowIndex++ };
|
||||||
|
headerRow.Append(CreateSummaryCell("A", headerRow.RowIndex!.Value, "Trend Dashboard", 1));
|
||||||
|
headerRow.Append(CreateSummaryCell("B", headerRow.RowIndex!.Value, "Current", 1));
|
||||||
|
headerRow.Append(CreateSummaryCell("C", headerRow.RowIndex!.Value, "Target", 1));
|
||||||
|
headerRow.Append(CreateSummaryCell("D", headerRow.RowIndex!.Value, "Delta", 1));
|
||||||
|
headerRow.Append(CreateSummaryCell("E", headerRow.RowIndex!.Value, "Status", 1));
|
||||||
|
sheetData.Append(headerRow);
|
||||||
|
|
||||||
|
var trendIndex = 0;
|
||||||
|
foreach (var trend in trendSeries.EnumerateArray())
|
||||||
|
{
|
||||||
|
var row = new Row { RowIndex = rowIndex++ };
|
||||||
|
var stripeStyle = trendIndex % 2 == 0 ? (uint)0 : (uint)2;
|
||||||
|
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, trend.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? "" : "", 1));
|
||||||
|
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, trend.SafeTryGetProperty("current", out var currentEl) ? currentEl.SafeGetString() ?? currentEl.ToString() : "", 3));
|
||||||
|
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, trend.SafeTryGetProperty("target", out var targetEl) ? targetEl.SafeGetString() ?? targetEl.ToString() : "", stripeStyle));
|
||||||
|
row.Append(CreateSummaryCell("D", row.RowIndex!.Value, trend.SafeTryGetProperty("delta", out var deltaEl) ? deltaEl.SafeGetString() ?? deltaEl.ToString() : "", stripeStyle));
|
||||||
|
row.Append(CreateSummaryCell("E", row.RowIndex!.Value, trend.SafeTryGetProperty("status", out var statusEl) ? statusEl.SafeGetString() ?? "" : "", stripeStyle));
|
||||||
|
sheetData.Append(row);
|
||||||
|
trendIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
|
private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
|
||||||
{
|
{
|
||||||
if (!summarySheet.SafeTryGetProperty(key, out var items) || items.ValueKind != JsonValueKind.Array || items.GetArrayLength() == 0)
|
if (!summarySheet.SafeTryGetProperty(key, out var items) || items.ValueKind != JsonValueKind.Array || items.GetArrayLength() == 0)
|
||||||
|
|||||||
@@ -133,10 +133,11 @@ public class HtmlSkill : IAgentTool
|
|||||||
var useToc = hasTocArg && tocVal.ValueKind == JsonValueKind.True;
|
var useToc = hasTocArg && tocVal.ValueKind == JsonValueKind.True;
|
||||||
var useNumbered = args.SafeTryGetProperty("numbered", out var numVal) && numVal.ValueKind == JsonValueKind.True;
|
var useNumbered = args.SafeTryGetProperty("numbered", out var numVal) && numVal.ValueKind == JsonValueKind.True;
|
||||||
var usePrint = args.SafeTryGetProperty("print", out var printVal) && printVal.ValueKind == JsonValueKind.True;
|
var usePrint = args.SafeTryGetProperty("print", out var printVal) && printVal.ValueKind == JsonValueKind.True;
|
||||||
var printHeader = args.SafeTryGetProperty("print_header", out var printHeaderEl) ? printHeaderEl.SafeGetString() : null;
|
var requestedPrintHeader = args.SafeTryGetProperty("print_header", out var printHeaderEl) ? printHeaderEl.SafeGetString() : null;
|
||||||
var printFooter = args.SafeTryGetProperty("print_footer", out var printFooterEl) ? printFooterEl.SafeGetString() : null;
|
var requestedPrintFooter = args.SafeTryGetProperty("print_footer", out var printFooterEl) ? printFooterEl.SafeGetString() : null;
|
||||||
var hasCover = args.SafeTryGetProperty("cover", out var coverVal) && coverVal.ValueKind == JsonValueKind.Object;
|
var hasCover = args.SafeTryGetProperty("cover", out var coverVal) && coverVal.ValueKind == JsonValueKind.Object;
|
||||||
var accentColor = args.SafeTryGetProperty("accent_color", out var accentEl) ? accentEl.SafeGetString() : null;
|
var accentColor = args.SafeTryGetProperty("accent_color", out var accentEl) ? accentEl.SafeGetString() : null;
|
||||||
|
var (printHeader, printFooter) = ResolvePrintFrame(title, usePrint, requestedPrintHeader, requestedPrintFooter, hasCover);
|
||||||
|
|
||||||
var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
||||||
if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
||||||
@@ -295,6 +296,8 @@ public class HtmlSkill : IAgentTool
|
|||||||
if (hasSections) features.Add("구조화섹션");
|
if (hasSections) features.Add("구조화섹션");
|
||||||
if (!string.IsNullOrEmpty(accentColor)) features.Add($"색상:{accentColor}");
|
if (!string.IsNullOrEmpty(accentColor)) features.Add($"색상:{accentColor}");
|
||||||
if (usePrint) features.Add("인쇄최적화");
|
if (usePrint) features.Add("인쇄최적화");
|
||||||
|
if (!string.IsNullOrWhiteSpace(printHeader) || !string.IsNullOrWhiteSpace(printFooter))
|
||||||
|
features.Add("print-frame");
|
||||||
var featureStr = features.Count > 0 ? $" [{string.Join(", ", features)}]" : "";
|
var featureStr = features.Count > 0 ? $" [{string.Join(", ", features)}]" : "";
|
||||||
var review = ArtifactQualityReviewService.ReviewHtml(
|
var review = ArtifactQualityReviewService.ReviewHtml(
|
||||||
title,
|
title,
|
||||||
@@ -383,6 +386,27 @@ public class HtmlSkill : IAgentTool
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (string? Header, string? Footer) ResolvePrintFrame(
|
||||||
|
string title,
|
||||||
|
bool usePrint,
|
||||||
|
string? requestedHeader,
|
||||||
|
string? requestedFooter,
|
||||||
|
bool hasCover)
|
||||||
|
{
|
||||||
|
if (!usePrint)
|
||||||
|
return (requestedHeader, requestedFooter);
|
||||||
|
|
||||||
|
var header = requestedHeader;
|
||||||
|
var footer = requestedFooter;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(header))
|
||||||
|
header = hasCover ? $"{title} | Executive Pack" : title;
|
||||||
|
if (string.IsNullOrWhiteSpace(footer))
|
||||||
|
footer = $"{DateTime.Now:yyyy-MM-dd} | AX Copilot";
|
||||||
|
|
||||||
|
return (header, footer);
|
||||||
|
}
|
||||||
|
|
||||||
private static string RenderHeading(JsonElement s)
|
private static string RenderHeading(JsonElement s)
|
||||||
{
|
{
|
||||||
var level = s.SafeTryGetProperty("level", out var lv) ? Math.Clamp(lv.GetInt32(), 1, 4) : 2;
|
var level = s.SafeTryGetProperty("level", out var lv) ? Math.Clamp(lv.GetInt32(), 1, 4) : 2;
|
||||||
|
|||||||
Reference in New Issue
Block a user