에이전트 루프와 코드 언어 지원, PPT 생성 품질을 함께 고도화
- AgentCommandQueue를 도입해 실행 중 추가 입력을 우선순위와 인터럽트 여부까지 포함해 처리하도록 정리함 - AgentToolResultBudget와 AgentQueryContextBuilder에 tool result preview 캐시를 연결해 긴 세션에서 축약 결과 재사용을 안정화함 - CodeLanguageCatalog를 추가해 코드 탭의 내장 언어 지원, 인덱싱 확장자, 시스템 프롬프트 언어 가이드, LSP 언어 판정을 한 카탈로그로 통합함 - 설정의 코드 탭에 지원 언어(LSP)와 코드 탭 기본 지원 언어를 명시적으로 표시하도록 보강함 - DocumentPlannerTool의 presentation 구조를 컨설팅형 스토리라인으로 정리하고, PptxSkill에 executive_summary/recommendation/roadmap/comparison/kpi_dashboard 레이아웃을 추가함 - pptx-creator 스킬을 AX native pptx_create 중심으로 재작성하고, 관련 회귀 테스트를 추가했으며 WorkspaceContextGeneratorTests의 nullable 경고도 정리함 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_impl\\ -p:IntermediateOutputPath=obj\\verify_impl\\ - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|AgentCommandQueueTests|AgentToolResultBudgetTests|DocumentPlannerPresentationTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_impl_tests\\ -p:IntermediateOutputPath=obj\\verify_impl_tests\\
This commit is contained in:
@@ -36,6 +36,10 @@ public class PptxSkill : IAgentTool
|
||||
"section (chapter divider), two_column (title+left+right), " +
|
||||
"table (title+real styled table), quote (styled quotation slide), blank, " +
|
||||
"image_full (full-slide background image), " +
|
||||
"executive_summary (consulting-style headline and takeaways), " +
|
||||
"recommendation (single recommendation with rationale), " +
|
||||
"roadmap (phase timeline), comparison (option comparison cards), " +
|
||||
"kpi_dashboard (metric cards and takeaways), " +
|
||||
"chart (native OpenXML chart — bar/line/pie with embedded data). " +
|
||||
"Built-in themes (each controls BOTH colors AND layout/composition): " +
|
||||
"professional (bar_left), modern (top_band+card), dark (center_bold), " +
|
||||
@@ -72,10 +76,17 @@ public class PptxSkill : IAgentTool
|
||||
Type = "array",
|
||||
Description =
|
||||
"Array of slide objects. Each slide: " +
|
||||
"{\"layout\": \"title|content|section|two_column|table|quote|blank|image_full|chart\", " +
|
||||
"{\"layout\": \"title|content|section|two_column|table|quote|blank|image_full|chart|executive_summary|recommendation|roadmap|comparison|kpi_dashboard\", " +
|
||||
"\"title\": \"...\", \"subtitle\": \"...\", " +
|
||||
"\"body\": \"bullet1\\nbullet2\\n - sub-bullet (IMPORTANT: write 5-8 detailed bullet points per slide to fill the space fully)\", " +
|
||||
"\"headline\": \"single-message headline for the slide\", " +
|
||||
"\"body\": \"3-5 concise evidence-backed bullets or short statements\", " +
|
||||
"\"left\": \"...\", \"right\": \"...\", " +
|
||||
"\"summary_points\": [\"key point 1\", \"key point 2\"], " +
|
||||
"\"recommendation\": \"recommended action or decision\", " +
|
||||
"\"next_steps\": [\"step 1\", \"step 2\"], " +
|
||||
"\"phases\": [{\"title\":\"Phase 1\",\"detail\":\"Design\",\"timeline\":\"Q1\",\"owner\":\"PM\"}], " +
|
||||
"\"options\": [{\"name\":\"Option A\",\"pros\":\"...\",\"cons\":\"...\",\"verdict\":\"Recommended\"}], " +
|
||||
"\"kpis\": [{\"label\":\"Revenue\",\"value\":\"12%\",\"trend\":\"YoY\",\"note\":\"Top-line growth\"}], " +
|
||||
"\"headers\": [\"col1\",\"col2\"], \"rows\": [[\"a\",\"b\"]], " +
|
||||
"\"quote\": \"Quote text\", \"author\": \"Author\", " +
|
||||
"\"image\": \"path/to/image.png (local file, embedded in slide; position: top_right|bottom_right|center|full|left_half|right_half; default: center)\", " +
|
||||
@@ -818,6 +829,21 @@ public class PptxSkill : IAgentTool
|
||||
case "chart":
|
||||
BuildChartSlide(slidePart, shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
|
||||
break;
|
||||
case "executive_summary":
|
||||
BuildExecutiveSummarySlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
|
||||
break;
|
||||
case "recommendation":
|
||||
BuildRecommendationSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
|
||||
break;
|
||||
case "roadmap":
|
||||
BuildRoadmapSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
|
||||
break;
|
||||
case "comparison":
|
||||
BuildComparisonSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
|
||||
break;
|
||||
case "kpi_dashboard":
|
||||
BuildKpiDashboardSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid);
|
||||
break;
|
||||
case "blank":
|
||||
break;
|
||||
default: // content
|
||||
@@ -1432,6 +1458,334 @@ public class PptxSkill : IAgentTool
|
||||
/// bar(세로 막대), line(꺾은선), pie(원형) 차트를 지원합니다.
|
||||
/// ChartPart에 내장 스프레드시트 데이터를 포함하므로 PowerPoint에서 편집 가능합니다.
|
||||
/// </summary>
|
||||
private sealed record PresentationCardItem(string Title, string Primary, string Secondary, string Badge);
|
||||
|
||||
private static void BuildExecutiveSummarySlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
const long M = 360000;
|
||||
AddRect(t, ref id, 0, 0, W, H, c.Bg);
|
||||
AddRect(t, ref id, 0, 0, W, 170000, c.Accent);
|
||||
|
||||
var title = CoalesceText(Str(s, "title"), "Executive Summary");
|
||||
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), title);
|
||||
var summaryPoints = GetStringList(s, "summary_points");
|
||||
if (summaryPoints.Count == 0)
|
||||
summaryPoints = GetStringList(s, "body");
|
||||
if (summaryPoints.Count == 0)
|
||||
summaryPoints = ["핵심 메시지를 3개 이하로 정리", "가장 중요한 수치와 근거를 포함", "의사결정자가 다음 행동을 바로 이해할 수 있도록 구성"];
|
||||
|
||||
var recommendation = CoalesceText(Str(s, "recommendation"), Str(s, "subtitle"), "권고안과 기대효과를 한 문장으로 정리");
|
||||
var kpis = GetStructuredItems(s, "kpis");
|
||||
|
||||
AddText(t, ref id, M, 220000, W - M * 2, 420000, title, 2200, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, M, 520000, W - M * 2, 760000, headline, 3200, c.Primary, bold: true, align: "l");
|
||||
|
||||
long leftX = M;
|
||||
long topY = 1420000;
|
||||
long gap = 220000;
|
||||
long leftW = (W - M * 2 - gap) * 58 / 100;
|
||||
long rightX = leftX + leftW + gap;
|
||||
long rightW = W - M - rightX;
|
||||
long topH = 2300000;
|
||||
|
||||
AddRoundedRect(t, ref id, leftX, topY, leftW, topH, c.BgAlt);
|
||||
AddText(t, ref id, leftX + 120000, topY + 70000, leftW - 240000, 300000, "Key Takeaways", 1800, c.Primary, bold: true, align: "l");
|
||||
AddBulletBody(t, ref id, leftX + 100000, topY + 360000, leftW - 200000, topH - 460000,
|
||||
string.Join("\n", summaryPoints.Select(x => $"- {x}")), 1700, c.TextDark, c.Accent);
|
||||
|
||||
AddRoundedRect(t, ref id, rightX, topY, rightW, topH, c.Primary);
|
||||
AddText(t, ref id, rightX + 120000, topY + 70000, rightW - 240000, 260000, "Recommendation", 1800, c.TextLight, bold: true, align: "l");
|
||||
AddTextEx(t, ref id, rightX + 120000, topY + 420000, rightW - 240000, 1100000, recommendation, 2200, c.TextLight, bold: true, italic: false, align: "l");
|
||||
var rationaleLines = GetStringList(s, "next_steps");
|
||||
if (rationaleLines.Count == 0)
|
||||
rationaleLines = GetStringList(s, "body").Take(2).ToList();
|
||||
if (rationaleLines.Count > 0)
|
||||
AddBulletBody(t, ref id, rightX + 100000, topY + 1500000, rightW - 200000, topH - 1620000,
|
||||
string.Join("\n", rationaleLines.Select(x => $"- {x}")), 1500, c.TextLight, c.Accent);
|
||||
|
||||
if (kpis.Count > 0)
|
||||
{
|
||||
var metricCount = Math.Min(3, kpis.Count);
|
||||
long cardY = topY + topH + 200000;
|
||||
long cardGap = 180000;
|
||||
long cardW = (W - M * 2 - cardGap * (metricCount - 1)) / metricCount;
|
||||
long cardH = 1050000;
|
||||
|
||||
for (int i = 0; i < metricCount; i++)
|
||||
{
|
||||
var metric = kpis[i];
|
||||
var cardX = M + i * (cardW + cardGap);
|
||||
AddRoundedRect(t, ref id, cardX, cardY, cardW, cardH, c.BgAlt);
|
||||
AddText(t, ref id, cardX + 90000, cardY + 70000, cardW - 180000, 220000, metric.Title, 1400, c.TextDark, bold: false, align: "l");
|
||||
AddText(t, ref id, cardX + 90000, cardY + 260000, cardW - 180000, 330000, CoalesceText(metric.Primary, "-"), 2600, c.Primary, bold: true, align: "l");
|
||||
AddText(t, ref id, cardX + 90000, cardY + 640000, cardW - 180000, 170000, CoalesceText(metric.Secondary, metric.Badge), 1300, c.Accent, bold: true, align: "l");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildRecommendationSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
const long M = 380000;
|
||||
AddRect(t, ref id, 0, 0, W, H, c.Bg);
|
||||
|
||||
var title = CoalesceText(Str(s, "title"), "Recommendation");
|
||||
var recommendation = CoalesceText(Str(s, "recommendation"), Str(s, "headline"), Str(s, "subtitle"), "권고안을 한 문장으로 제시");
|
||||
var reasons = GetStringList(s, "summary_points");
|
||||
if (reasons.Count == 0)
|
||||
reasons = GetStringList(s, "body");
|
||||
var nextSteps = GetStringList(s, "next_steps");
|
||||
|
||||
AddText(t, ref id, M, 220000, W - M * 2, 360000, title, 2200, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, M, 520000, W - M * 2, 420000, recommendation, 3000, c.Primary, bold: true, align: "l");
|
||||
|
||||
long mainY = 1220000;
|
||||
long gap = 220000;
|
||||
long leftW = (W - M * 2 - gap) * 52 / 100;
|
||||
long rightX = M + leftW + gap;
|
||||
long rightW = W - M - rightX;
|
||||
long boxH = 2450000;
|
||||
|
||||
AddRoundedRect(t, ref id, M, mainY, leftW, boxH, c.Primary);
|
||||
AddText(t, ref id, M + 120000, mainY + 80000, leftW - 240000, 240000, "Recommended Move", 1800, c.TextLight, bold: true, align: "l");
|
||||
AddTextEx(t, ref id, M + 120000, mainY + 400000, leftW - 240000, 1200000, recommendation, 2400, c.TextLight, bold: true, italic: false, align: "l");
|
||||
if (nextSteps.Count > 0)
|
||||
{
|
||||
AddText(t, ref id, M + 120000, mainY + 1780000, leftW - 240000, 180000, "Immediate Actions", 1600, c.Accent, bold: true, align: "l");
|
||||
AddBulletBody(t, ref id, M + 100000, mainY + 1990000, leftW - 200000, boxH - 2090000,
|
||||
string.Join("\n", nextSteps.Take(3).Select(x => $"- {x}")), 1450, c.TextLight, c.Accent);
|
||||
}
|
||||
|
||||
AddRoundedRect(t, ref id, rightX, mainY, rightW, boxH, c.BgAlt);
|
||||
AddText(t, ref id, rightX + 120000, mainY + 80000, rightW - 240000, 240000, "Why This Wins", 1800, c.Primary, bold: true, align: "l");
|
||||
AddBulletBody(t, ref id, rightX + 100000, mainY + 360000, rightW - 200000, boxH - 480000,
|
||||
string.Join("\n", reasons.Take(5).Select(x => $"- {x}")), 1600, c.TextDark, c.Accent);
|
||||
}
|
||||
|
||||
private static void BuildRoadmapSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
const long M = 360000;
|
||||
AddRect(t, ref id, 0, 0, W, H, c.Bg);
|
||||
|
||||
var title = CoalesceText(Str(s, "title"), "Implementation Roadmap");
|
||||
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "단계별 우선순위와 산출물을 한 눈에 정리");
|
||||
var phases = GetStructuredItems(s, "phases");
|
||||
if (phases.Count == 0)
|
||||
{
|
||||
phases =
|
||||
[
|
||||
new("Phase 1", "진단 및 설계", "0-30일", "PM"),
|
||||
new("Phase 2", "구현 및 검증", "30-60일", "Delivery"),
|
||||
new("Phase 3", "확산 및 운영정착", "60-90일", "Business")
|
||||
];
|
||||
}
|
||||
|
||||
AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l");
|
||||
AddRect(t, ref id, M, 1260000, W - M * 2, 12000, c.Accent);
|
||||
|
||||
var phaseCount = Math.Min(4, phases.Count);
|
||||
long gap = 160000;
|
||||
long laneY = 1540000;
|
||||
long laneW = (W - M * 2 - gap * (phaseCount - 1)) / phaseCount;
|
||||
long laneH = 2100000;
|
||||
|
||||
for (int i = 0; i < phaseCount; i++)
|
||||
{
|
||||
var phase = phases[i];
|
||||
var laneX = M + i * (laneW + gap);
|
||||
var fill = i % 2 == 0 ? c.BgAlt : c.Bg;
|
||||
AddRoundedRect(t, ref id, laneX, laneY, laneW, laneH, fill);
|
||||
AddRect(t, ref id, laneX, laneY, laneW, 160000, i % 2 == 0 ? c.Primary : c.Accent);
|
||||
AddText(t, ref id, laneX + 90000, laneY + 220000, laneW - 180000, 240000, phase.Title, 1800, c.Primary, bold: true, align: "l");
|
||||
AddText(t, ref id, laneX + 90000, laneY + 520000, laneW - 180000, 520000, CoalesceText(phase.Primary, "핵심 과업 정의"), 1500, c.TextDark, bold: false, align: "l");
|
||||
AddText(t, ref id, laneX + 90000, laneY + 1200000, laneW - 180000, 180000, CoalesceText(phase.Secondary, "기간 미정"), 1350, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, laneX + 90000, laneY + 1450000, laneW - 180000, 180000, CoalesceText(phase.Badge, "담당 미정"), 1250, c.TextDark, bold: false, align: "l");
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildComparisonSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
const long M = 340000;
|
||||
AddRect(t, ref id, 0, 0, W, H, c.Bg);
|
||||
|
||||
var title = CoalesceText(Str(s, "title"), "Option Comparison");
|
||||
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "대안별 장단점과 권고안을 비교");
|
||||
var options = GetStructuredItems(s, "options");
|
||||
if (options.Count == 0)
|
||||
{
|
||||
options =
|
||||
[
|
||||
new("Option A", "빠른 적용", "확장성 제한", "Fastest"),
|
||||
new("Option B", "균형 잡힌 투자", "의사결정 필요", "Recommended"),
|
||||
new("Option C", "장기 최적화", "리드타임 길음", "Strategic")
|
||||
];
|
||||
}
|
||||
|
||||
AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l");
|
||||
|
||||
var optionCount = Math.Min(3, options.Count);
|
||||
long gap = 160000;
|
||||
long cardY = 1360000;
|
||||
long cardH = 2600000;
|
||||
long cardW = (W - M * 2 - gap * (optionCount - 1)) / optionCount;
|
||||
|
||||
for (int i = 0; i < optionCount; i++)
|
||||
{
|
||||
var option = options[i];
|
||||
var cardX = M + i * (cardW + gap);
|
||||
AddRoundedRect(t, ref id, cardX, cardY, cardW, cardH, c.BgAlt);
|
||||
AddRect(t, ref id, cardX, cardY, cardW, 140000, i == 1 ? c.Accent : c.Primary);
|
||||
AddText(t, ref id, cardX + 90000, cardY + 220000, cardW - 180000, 240000, option.Title, 1800, c.Primary, bold: true, align: "l");
|
||||
AddText(t, ref id, cardX + 90000, cardY + 560000, cardW - 180000, 180000, "Pros", 1400, c.Accent, bold: true, align: "l");
|
||||
AddTextEx(t, ref id, cardX + 90000, cardY + 760000, cardW - 180000, 520000, CoalesceText(option.Primary, "-"), 1450, c.TextDark, bold: false, italic: false, align: "l");
|
||||
AddText(t, ref id, cardX + 90000, cardY + 1480000, cardW - 180000, 180000, "Risks", 1400, c.Accent, bold: true, align: "l");
|
||||
AddTextEx(t, ref id, cardX + 90000, cardY + 1680000, cardW - 180000, 460000, CoalesceText(option.Secondary, "-"), 1450, c.TextDark, bold: false, italic: false, align: "l");
|
||||
AddText(t, ref id, cardX + 90000, cardY + 2280000, cardW - 180000, 160000, CoalesceText(option.Badge, ""), 1300, c.Primary, bold: true, align: "l");
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildKpiDashboardSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
const long M = 360000;
|
||||
AddRect(t, ref id, 0, 0, W, H, c.Bg);
|
||||
|
||||
var title = CoalesceText(Str(s, "title"), "KPI Dashboard");
|
||||
var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "핵심 지표의 현재 상태와 시사점");
|
||||
var metrics = GetStructuredItems(s, "kpis");
|
||||
if (metrics.Count == 0)
|
||||
{
|
||||
metrics =
|
||||
[
|
||||
new("Revenue", "12%", "YoY", "Strong"),
|
||||
new("Cost", "-8%", "vs plan", "Improving"),
|
||||
new("NPS", "61", "survey", "Stable"),
|
||||
new("Delivery", "94%", "SLA", "On track")
|
||||
];
|
||||
}
|
||||
|
||||
AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l");
|
||||
|
||||
long cardTop = 1320000;
|
||||
long gap = 180000;
|
||||
long cardW = (W - M * 2 - gap) / 2;
|
||||
long cardH = 1160000;
|
||||
|
||||
for (int i = 0; i < Math.Min(4, metrics.Count); i++)
|
||||
{
|
||||
var metric = metrics[i];
|
||||
long row = i / 2;
|
||||
long col = i % 2;
|
||||
long x = M + col * (cardW + gap);
|
||||
long y = cardTop + row * (cardH + 180000);
|
||||
AddRoundedRect(t, ref id, x, y, cardW, cardH, c.BgAlt);
|
||||
AddText(t, ref id, x + 100000, y + 80000, cardW - 200000, 180000, metric.Title, 1450, c.TextDark, bold: false, align: "l");
|
||||
AddText(t, ref id, x + 100000, y + 280000, cardW - 200000, 340000, CoalesceText(metric.Primary, "-"), 2800, c.Primary, bold: true, align: "l");
|
||||
AddText(t, ref id, x + 100000, y + 700000, cardW - 200000, 150000, CoalesceText(metric.Secondary, ""), 1300, c.Accent, bold: true, align: "l");
|
||||
AddText(t, ref id, x + 100000, y + 900000, cardW - 200000, 140000, CoalesceText(metric.Badge, ""), 1200, c.TextDark, bold: false, align: "l");
|
||||
}
|
||||
|
||||
var takeawayLines = GetStringList(s, "summary_points");
|
||||
if (takeawayLines.Count == 0)
|
||||
takeawayLines = GetStringList(s, "body");
|
||||
if (takeawayLines.Count > 0)
|
||||
{
|
||||
AddRect(t, ref id, M, H - 650000, W - M * 2, 9000, c.Accent);
|
||||
AddText(t, ref id, M, H - 560000, W - M * 2, 140000, "Implication", 1500, c.Primary, bold: true, align: "l");
|
||||
AddTextEx(t, ref id, M, H - 380000, W - M * 2, 220000, takeawayLines[0], 1450, c.TextDark, bold: false, italic: false, align: "l");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> GetStringList(JsonElement source, string propertyName)
|
||||
{
|
||||
if (!source.SafeTryGetProperty(propertyName, out var value))
|
||||
return [];
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return value.EnumerateArray()
|
||||
.Select(item => item.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => item.SafeGetString() ?? string.Empty,
|
||||
JsonValueKind.Object => CoalesceText(
|
||||
FirstNonEmpty(item, "title", "name", "label", "value", "detail", "note"),
|
||||
item.ToString()),
|
||||
_ => item.ToString()
|
||||
})
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return (value.SafeGetString() ?? string.Empty)
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static List<PresentationCardItem> GetStructuredItems(JsonElement source, string propertyName)
|
||||
{
|
||||
var items = new List<PresentationCardItem>();
|
||||
if (!source.SafeTryGetProperty(propertyName, out var value) || value.ValueKind != JsonValueKind.Array)
|
||||
return items;
|
||||
|
||||
foreach (var item in value.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var text = item.SafeGetString() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
items.Add(new PresentationCardItem(text, string.Empty, string.Empty, string.Empty));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
continue;
|
||||
|
||||
items.Add(new PresentationCardItem(
|
||||
CoalesceText(FirstNonEmpty(item, "title", "name", "label"), "Item"),
|
||||
CoalesceText(FirstNonEmpty(item, "value", "detail", "pros", "timeline", "summary"), string.Empty),
|
||||
CoalesceText(FirstNonEmpty(item, "note", "cons", "trend", "owner", "secondary"), string.Empty),
|
||||
CoalesceText(FirstNonEmpty(item, "badge", "verdict", "status", "tag"), string.Empty)));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(JsonElement source, params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!source.SafeTryGetProperty(key, out var value))
|
||||
continue;
|
||||
|
||||
var text = value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => value.SafeGetString(),
|
||||
JsonValueKind.Number => value.ToString(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
return text;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string CoalesceText(params string?[] values)
|
||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
|
||||
|
||||
private static void BuildChartSlide(SlidePart slidePart, ShapeTree tree, JsonElement s,
|
||||
FullTheme theme, long W, long H, ref uint id)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user