056c47581f
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that presents the 4-section PDP pilot batch back to Umar, compliance, and the board in an editorial format. Adds the full data layer that drives it: 5 source-backed per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated product FAQs, and the Matrixify-ready CSV exports for Lewis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
207 lines
9.1 KiB
PowerShell
207 lines
9.1 KiB
PowerShell
param(
|
|
[switch]$Force
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
$AssetRoot = if ($env:ASSET_ROOT) { $env:ASSET_ROOT } else { (Resolve-Path (Join-Path $Root "..\..\jv-assets")).Path }
|
|
$OutRoot = Join-Path $Root "content_population_exports"
|
|
$ProductsRoot = Join-Path $Root "data\content\products"
|
|
$IntelRoot = Join-Path $Root "data\intelligence"
|
|
$GeneratedAt = (Get-Date).ToUniversalTime().ToString("o")
|
|
|
|
New-Item -ItemType Directory -Force -Path $AssetRoot | Out-Null
|
|
|
|
function Read-JsonFile($Path, $Fallback = $null) {
|
|
if (-not (Test-Path -LiteralPath $Path)) { return $Fallback }
|
|
return (Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json)
|
|
}
|
|
|
|
function ConvertTo-PlainText($Value) {
|
|
if ($null -eq $Value) { return "" }
|
|
if ($Value -is [array]) { return (($Value | ForEach-Object { ConvertTo-PlainText $_ }) -join " · ") }
|
|
$text = [string]$Value
|
|
$text = $text -replace "[\u2013\u2014]", "-"
|
|
$text = $text -replace "[^\u0009\u000A\u000D\u0020-\u007E]", ""
|
|
$text = $text -replace "\s+", " "
|
|
return $text.Trim()
|
|
}
|
|
|
|
function Wrap-Text([System.Drawing.Graphics]$Graphics, [string]$Text, [System.Drawing.Font]$Font, [int]$MaxWidth, [int]$MaxLines = 4) {
|
|
$words = (ConvertTo-PlainText $Text) -split "\s+"
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
$line = ""
|
|
foreach ($word in $words) {
|
|
$candidate = if ($line) { "$line $word" } else { $word }
|
|
if ($Graphics.MeasureString($candidate, $Font).Width -le $MaxWidth) {
|
|
$line = $candidate
|
|
} else {
|
|
if ($line) { $lines.Add($line) }
|
|
$line = $word
|
|
if ($lines.Count -ge $MaxLines) { break }
|
|
}
|
|
}
|
|
if ($line -and $lines.Count -lt $MaxLines) { $lines.Add($line) }
|
|
if ($lines.Count -eq $MaxLines -and $words.Count -gt 0) {
|
|
$last = $lines[$lines.Count - 1]
|
|
if ($Graphics.MeasureString("$last ...", $Font).Width -le $MaxWidth) { $lines[$lines.Count - 1] = "$last ..." }
|
|
}
|
|
return $lines.ToArray()
|
|
}
|
|
|
|
function Get-ProductThemes($Product, $Sku) {
|
|
$themes = New-Object System.Collections.Generic.List[string]
|
|
foreach ($benefit in @($Product.metafields.pdpProductBenefits)) {
|
|
$t = ConvertTo-PlainText $benefit
|
|
if ($t) { $themes.Add($t) }
|
|
}
|
|
$aspects = Read-JsonFile (Join-Path $IntelRoot "$Sku\review-aspects.json")
|
|
foreach ($aspect in @($aspects.aspects | Select-Object -First 3)) {
|
|
$t = ConvertTo-PlainText $aspect.aspect
|
|
if ($t -and -not $themes.Contains($t)) { $themes.Add($t) }
|
|
}
|
|
if ($themes.Count -eq 0) {
|
|
$themes.Add("Customer-language extraction pending")
|
|
$themes.Add("Evidence review required before approval")
|
|
$themes.Add("Draft visual asset for staging QA")
|
|
}
|
|
return @($themes | Select-Object -First 4)
|
|
}
|
|
|
|
function Draw-DraftAsset($TargetPath, $Product, [string]$Field) {
|
|
$isMobile = $Field -eq "pdpResultsMobile"
|
|
$isCompare = $Field -eq "pdpHowWeCompare"
|
|
$width = if ($isMobile) { 1080 } else { 1600 }
|
|
$height = if ($isMobile) { 1350 } else { 1000 }
|
|
|
|
$bitmap = New-Object System.Drawing.Bitmap($width, $height)
|
|
$g = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
|
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
|
|
|
|
$bg = [System.Drawing.Color]::FromArgb(247, 248, 255)
|
|
$brand = [System.Drawing.Color]::FromArgb(50, 50, 125)
|
|
$accent = [System.Drawing.Color]::FromArgb(245, 158, 11)
|
|
$muted = [System.Drawing.Color]::FromArgb(91, 97, 120)
|
|
$border = [System.Drawing.Color]::FromArgb(220, 225, 245)
|
|
$white = [System.Drawing.Color]::White
|
|
|
|
$g.Clear($bg)
|
|
$brandBrush = New-Object System.Drawing.SolidBrush($brand)
|
|
$accentBrush = New-Object System.Drawing.SolidBrush($accent)
|
|
$mutedBrush = New-Object System.Drawing.SolidBrush($muted)
|
|
$whiteBrush = New-Object System.Drawing.SolidBrush($white)
|
|
$borderPen = New-Object System.Drawing.Pen($border, 4)
|
|
$brandPen = New-Object System.Drawing.Pen($brand, 8)
|
|
|
|
$titleFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 52 } else { 58 }), [System.Drawing.FontStyle]::Bold)
|
|
$subFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 30 } else { 32 }), [System.Drawing.FontStyle]::Regular)
|
|
$bodyFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 28 } else { 30 }), [System.Drawing.FontStyle]::Regular)
|
|
$smallFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 22 } else { 22 }), [System.Drawing.FontStyle]::Bold)
|
|
$draftFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 24 } else { 24 }), [System.Drawing.FontStyle]::Bold)
|
|
|
|
$margin = if ($isMobile) { 72 } else { 96 }
|
|
$contentWidth = $width - (2 * $margin)
|
|
|
|
$g.FillRectangle($brandBrush, 0, 0, $width, $(if ($isMobile) { 118 } else { 110 }))
|
|
$g.DrawString("JUST VITAMINS", $smallFont, $whiteBrush, $margin, 36)
|
|
$g.DrawString("DRAFT ASSET - NEEDS HUMAN IMAGE APPROVAL", $draftFont, $accentBrush, $width - $margin - 520, 36)
|
|
|
|
$y = if ($isMobile) { 170 } else { 160 }
|
|
$assetLabel = if ($Field -eq "pdpResults") { "Results / benefit visual" } elseif ($Field -eq "pdpResultsMobile") { "Mobile results visual" } else { "How we compare visual" }
|
|
$g.DrawString($assetLabel.ToUpperInvariant(), $smallFont, $accentBrush, $margin, $y)
|
|
$y += 48
|
|
|
|
foreach ($line in (Wrap-Text $g (ConvertTo-PlainText $Product.name) $titleFont $contentWidth 2)) {
|
|
$g.DrawString($line, $titleFont, $brandBrush, $margin, $y)
|
|
$y += [int]($titleFont.GetHeight($g) + 6)
|
|
}
|
|
$y += 24
|
|
|
|
$boxHeight = if ($isMobile) { 560 } else { 410 }
|
|
$rect = New-Object System.Drawing.Rectangle($margin, $y, $contentWidth, $boxHeight)
|
|
$g.FillRectangle($whiteBrush, $rect)
|
|
$g.DrawRectangle($borderPen, $rect)
|
|
|
|
$innerX = $margin + 44
|
|
$innerY = $y + 42
|
|
$headline = if ($isCompare) { "Comparison content is queued for review" } else { "Customer-led message scaffold" }
|
|
$g.DrawString($headline, $subFont, $brandBrush, $innerX, $innerY)
|
|
$innerY += 72
|
|
|
|
$themes = Get-ProductThemes $Product $Product.sku
|
|
$idx = 1
|
|
foreach ($theme in $themes) {
|
|
$g.FillEllipse($accentBrush, $innerX, $innerY + 8, 30, 30)
|
|
$g.DrawString([string]$idx, $smallFont, $whiteBrush, $innerX + 8, $innerY + 6)
|
|
foreach ($line in (Wrap-Text $g $theme $bodyFont ($contentWidth - 130) 2)) {
|
|
$g.DrawString($line, $bodyFont, $mutedBrush, $innerX + 54, $innerY)
|
|
$innerY += [int]($bodyFont.GetHeight($g) + 3)
|
|
}
|
|
$innerY += 20
|
|
$idx += 1
|
|
}
|
|
|
|
$footerY = $height - 120
|
|
$g.DrawLine($borderPen, $margin, $footerY - 26, $width - $margin, $footerY - 26)
|
|
$footer = "Generated from dashboard evidence only. Not approved. Replace with final creative after quote-level, compliance, and image QA."
|
|
foreach ($line in (Wrap-Text $g $footer $smallFont $contentWidth 2)) {
|
|
$g.DrawString($line, $smallFont, $mutedBrush, $margin, $footerY)
|
|
$footerY += [int]($smallFont.GetHeight($g) + 4)
|
|
}
|
|
|
|
$bitmap.Save($TargetPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
|
|
$g.Dispose()
|
|
$bitmap.Dispose()
|
|
foreach ($obj in @($brandBrush, $accentBrush, $mutedBrush, $whiteBrush, $borderPen, $brandPen, $titleFont, $subFont, $bodyFont, $smallFont, $draftFont)) { $obj.Dispose() }
|
|
}
|
|
|
|
$rows = New-Object System.Collections.Generic.List[object]
|
|
$products = Get-ChildItem -LiteralPath $ProductsRoot -Filter "*.json" | ForEach-Object { Read-JsonFile $_.FullName }
|
|
|
|
foreach ($product in $products) {
|
|
foreach ($field in @("pdpResults", "pdpResultsMobile", "pdpHowWeCompare")) {
|
|
$filename = ConvertTo-PlainText $product.metafields.$field
|
|
if (-not $filename) { continue }
|
|
$target = Join-Path $AssetRoot $filename
|
|
$exists = Test-Path -LiteralPath $target
|
|
if ($exists -and -not $Force) {
|
|
$status = "preserved_existing"
|
|
} else {
|
|
Draw-DraftAsset $target $product $field
|
|
$status = if ($exists) { "regenerated_draft_png" } else { "generated_draft_png" }
|
|
}
|
|
$rows.Add([pscustomobject]@{
|
|
sku = $product.sku
|
|
field = $field
|
|
filename = $filename
|
|
path = $target
|
|
status = $status
|
|
approvalStatus = "draft_needs_human_image_approval"
|
|
})
|
|
}
|
|
}
|
|
|
|
$json = [pscustomobject]@{
|
|
generatedAt = $GeneratedAt
|
|
assetRoot = $AssetRoot
|
|
totalProductPdpAssets = $rows.Count
|
|
generated = @($rows | Where-Object { $_.status -like "generated*" }).Count
|
|
regenerated = @($rows | Where-Object { $_.status -like "regenerated*" }).Count
|
|
preserved = @($rows | Where-Object { $_.status -eq "preserved_existing" }).Count
|
|
caveat = "Draft PNGs are staging/review assets generated from current dashboard evidence. They are not human-approved final creative."
|
|
rows = $rows
|
|
}
|
|
|
|
$json | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $OutRoot "draft_pdp_asset_generation.json") -Encoding UTF8
|
|
$rows | Export-Csv -LiteralPath (Join-Path $OutRoot "draft_pdp_asset_generation.csv") -NoTypeInformation -Encoding UTF8
|
|
|
|
Write-Host "Draft PDP assets represented: $($rows.Count)"
|
|
Write-Host "Generated: $(@($rows | Where-Object { $_.status -eq 'generated_draft_png' }).Count)"
|
|
Write-Host "Regenerated: $(@($rows | Where-Object { $_.status -eq 'regenerated_draft_png' }).Count)"
|
|
Write-Host "Preserved existing: $(@($rows | Where-Object { $_.status -eq 'preserved_existing' }).Count)"
|