Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | 35x 35x 35x 35x 35x 4042x 4042x 3756x 4042x 4042x 3756x 3756x 4042x 4042x 35x 4042x 4042x 4042x 4042x 4036x 4036x 4042x 4042x 4042x 4042x 4042x 4042x 4042x 1009x 1009x 1009x 1009x 1009x 4036x 4036x 4036x 1009x 1009x 201x 201x 15876x 15876x 201x 1009x 201x 201x 201x 201x | /**
* @module data-transformers/content-generators/stakeholder-swot-section
* @description Generates a multi-stakeholder SWOT analysis section that provides
* deep intelligence perspectives across all impacted stakeholders. Each stakeholder
* receives its own SWOT quadrant analysis, enabling comprehensive strategic assessment.
*
* Used by agentic workflows to produce deep inspection analysis articles that
* cover government, opposition, civil society, and other affected parties.
*
* @author Hack23 AB
* @license Apache-2.0
*/
import { escapeHtml } from '../../html-utils.js';
import type { Language } from '../../types/language.js';
import type { TemplateSection, SwotData, SwotEntry, SwotImpact, TrendDirection } from '../../types/article.js';
import { L } from '../helpers.js';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** Stakeholder category for multi-perspective intelligence analysis */
export type StakeholderCategory =
| 'government'
| 'opposition'
| 'private'
| 'civil-society'
| 'municipal'
| 'international'
| 'media'
| 'academia'
| 'labor';
/** A single stakeholder with their own SWOT analysis */
export interface StakeholderSwot {
/** Stakeholder name (e.g. "Government Coalition", "Opposition Parties") */
name: string;
/** Role or description of the stakeholder */
role?: string;
/** Stakeholder category for analysis routing */
category?: StakeholderCategory;
/** SWOT data for this stakeholder */
swot: SwotData;
/** Document IDs (dok_id) that provide evidence for this SWOT analysis */
evidenceRefs?: string[];
/** Confidence level for this stakeholder analysis based on evidence quantity/quality */
confidenceLevel?: 'high' | 'medium' | 'low';
}
/** Options for the multi-stakeholder SWOT section generator */
export interface StakeholderSwotSectionOptions {
/** Title for the overall analysis section */
title?: string;
/** Array of stakeholder SWOT analyses */
stakeholders: StakeholderSwot[];
/** Target language for labels */
lang: Language | string;
/** Overall strategic context note */
strategicContext?: string;
}
// ---------------------------------------------------------------------------
// Localised labels
// ---------------------------------------------------------------------------
const SECTION_TITLES: Readonly<Record<string, string>> = {
en: 'Multi-Stakeholder SWOT Analysis',
sv: 'Intressentanalys (SWOT)',
da: 'Interessentanalyse (SWOT)',
no: 'Interessentanalyse (SWOT)',
fi: 'Sidosryhmäanalyysi (SWOT)',
de: 'Stakeholder-SWOT-Analyse',
fr: 'Analyse SWOT multi-parties prenantes',
es: 'Análisis SWOT de múltiples partes interesadas',
nl: 'Stakeholder SWOT-analyse',
ar: 'تحليل SWOT لأصحاب المصلحة',
he: 'ניתוח SWOT לבעלי עניין',
ja: '利害関係者SWOT分析',
ko: '이해관계자 SWOT 분석',
zh: '利益相关方SWOT分析',
};
const STRATEGIC_CONTEXT_LABELS: Readonly<Record<string, string>> = {
en: 'Strategic Context',
sv: 'Strategisk kontext',
da: 'Strategisk kontekst',
no: 'Strategisk kontekst',
fi: 'Strateginen konteksti',
de: 'Strategischer Kontext',
fr: 'Contexte stratégique',
es: 'Contexto estratégico',
nl: 'Strategische context',
ar: 'السياق الاستراتيجي',
he: 'הקשר אסטרטגי',
ja: '戦略的背景',
ko: '전략적 맥락',
zh: '战略背景',
};
// ---------------------------------------------------------------------------
// Extended entry interface (AI-generated SWOT entries carry extra metadata)
// ---------------------------------------------------------------------------
/**
* Extended SwotEntry with AI-analysis fields.
* These fields are optional so the renderer remains backward-compatible with
* plain `SwotEntry` objects that lack them.
*/
interface EnhancedSwotEntry extends SwotEntry {
/** Human-readable explanation for why this item was included */
justification?: string;
/** Direction this factor is heading */
trendDirection?: TrendDirection;
/** Supporting quantitative evidence (e.g. "73% majority", "SEK 2.1 bn") */
quantitativeEvidence?: string;
}
// ---------------------------------------------------------------------------
// Trend indicator helper
// ---------------------------------------------------------------------------
const TREND_SYMBOLS: Readonly<Record<TrendDirection, string>> = {
improving: '↑',
stable: '→',
deteriorating: '↓',
};
const TREND_CLASSES: Readonly<Record<TrendDirection, string>> = {
improving: 'swot-trend--improving',
stable: 'swot-trend--stable',
deteriorating: 'swot-trend--deteriorating',
};
/** Localised i18n keys for trend direction (used in aria-label) */
const TREND_LABEL_KEYS: Readonly<Record<TrendDirection, string>> = {
improving: 'swotTrendImproving',
stable: 'swotTrendStable',
deteriorating: 'swotTrendDeteriorating',
};
function trendIndicator(entry: EnhancedSwotEntry, lbl: (key: string) => string): string {
const dir = entry.trendDirection;
if (!dir) return '';
const sym = TREND_SYMBOLS[dir] ?? '';
const cls = TREND_CLASSES[dir] ?? '';
Iif (!sym) return ''; // Guard: unknown direction value → skip indicator
const labelKey = TREND_LABEL_KEYS[dir];
const raw = labelKey ? lbl(labelKey) : dir;
// If the label lookup returned the key itself (incomplete label map), fall back to the direction name
const ariaLabel = (raw === labelKey) ? dir : raw;
return ` <span class="swot-trend ${cls}" role="img" aria-label="${escapeHtml(ariaLabel)}">${sym}</span>`;
}
// ---------------------------------------------------------------------------
// Impact badge helper (shared with swot-section.ts pattern)
// ---------------------------------------------------------------------------
const IMPACT_CLASSES: Readonly<Record<SwotImpact, string>> = {
high: 'swot-impact--high',
medium: 'swot-impact--medium',
low: 'swot-impact--low',
};
function impactBadge(impact: SwotImpact | undefined, lbl: (key: string) => string): string {
Iif (!impact) return '';
const keys: Record<SwotImpact, string> = { high: 'swotImpactHigh', medium: 'swotImpactMedium', low: 'swotImpactLow' };
const label = lbl(keys[impact] ?? keys.medium);
return ` <span class="swot-impact ${IMPACT_CLASSES[impact] ?? IMPACT_CLASSES.medium}">[${escapeHtml(label)}]</span>`;
}
// ---------------------------------------------------------------------------
// Quadrant renderer
// ---------------------------------------------------------------------------
function renderEntries(entries: SwotEntry[], lbl: (key: string) => string): string {
Iif (!entries || entries.length === 0) return '';
return entries.map(e => {
const enhanced = e as EnhancedSwotEntry;
const badges = impactBadge(e.impact, lbl) + trendIndicator(enhanced, lbl);
const quantEvidence = enhanced.quantitativeEvidence
? ` <span class="swot-evidence">(${escapeHtml(enhanced.quantitativeEvidence)})</span>`
: '';
const justLabel = lbl('swotJustification');
// lbl() returns the key name when no translation is found; detect that and use English fallback
const justSummary = (justLabel !== 'swotJustification') ? justLabel : 'Analysis';
const justification = enhanced.justification?.trim()
? `\n <details class="swot-justification"><summary>${escapeHtml(justSummary)}</summary><p>${escapeHtml(enhanced.justification.trim())}</p></details>`
: '';
return ` <li>${escapeHtml(e.text)}${badges}${quantEvidence}${justification}</li>`;
}).join('\n');
}
function renderStakeholderSwot(stakeholder: StakeholderSwot, lbl: (key: string) => string): string {
const { name, role, swot } = stakeholder;
const roleLine = role ? `\n <p class="stakeholder-role"><em>${escapeHtml(role)}</em></p>` : '';
const quadrants: string[] = [];
const sections: Array<{ key: string; entries: SwotEntry[]; cssClass: string }> = [
{ key: 'swotStrengths', entries: swot.strengths, cssClass: 'swot-strengths' },
{ key: 'swotWeaknesses', entries: swot.weaknesses, cssClass: 'swot-weaknesses' },
{ key: 'swotOpportunities', entries: swot.opportunities, cssClass: 'swot-opportunities' },
{ key: 'swotThreats', entries: swot.threats, cssClass: 'swot-threats' },
];
for (const sec of sections) {
const items = renderEntries(sec.entries, lbl);
Eif (items) {
quadrants.push(` <div class="swot-quadrant ${sec.cssClass}">
<h4>${escapeHtml(lbl(sec.key))}</h4>
<ul>
${items}
</ul>
</div>`);
}
}
const contextNote = swot.context?.trim()
? `\n <p class="swot-context"><em>${escapeHtml(swot.context.trim())}</em></p>`
: '';
return ` <div class="stakeholder-swot-card">
<h3>${escapeHtml(name)}</h3>${roleLine}
<div class="swot-grid">
${quadrants.join('\n')}
</div>${contextNote}
</div>`;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Generate a multi-stakeholder SWOT analysis section.
*
* Returns a `TemplateSection` that can be appended to `ArticleData.sections`.
* Each stakeholder gets their own SWOT matrix, providing comprehensive
* multi-perspective intelligence analysis.
*
* @example
* ```ts
* import { generateStakeholderSwotSection } from './content-generators/stakeholder-swot-section.js';
*
* const section = generateStakeholderSwotSection({
* stakeholders: [
* {
* name: 'Government Coalition',
* role: 'Tidö Agreement parties (M, KD, L + SD)',
* swot: {
* strengths: [{ text: 'Parliamentary majority', impact: 'high' }],
* weaknesses: [{ text: 'Internal policy disagreements', impact: 'medium' }],
* opportunities: [{ text: 'Economic recovery momentum', impact: 'high' }],
* threats: [{ text: 'Rising opposition poll numbers', impact: 'medium' }],
* },
* },
* {
* name: 'Opposition',
* role: 'S, V, C, MP',
* swot: {
* strengths: [{ text: 'Strong polling position', impact: 'high' }],
* weaknesses: [{ text: 'Coalition formation uncertainty', impact: 'medium' }],
* opportunities: [{ text: 'Government policy failures', impact: 'high' }],
* threats: [{ text: 'Internal divisions on migration', impact: 'medium' }],
* },
* },
* ],
* lang: 'en',
* strategicContext: 'Ahead of the 2026 election, coalition dynamics are shifting.',
* });
* ```
*/
export function generateStakeholderSwotSection(opts: StakeholderSwotSectionOptions): TemplateSection {
const { stakeholders, lang, strategicContext } = opts;
const lbl = (key: string): string => {
const val = L(lang, key);
return typeof val === 'string' ? val : key;
};
const titleText = opts.title?.trim() || SECTION_TITLES[lang as string] || SECTION_TITLES.en;
const cards = stakeholders.map(s => renderStakeholderSwot(s, lbl)).join('\n');
const contextLabel = STRATEGIC_CONTEXT_LABELS[lang as string] || STRATEGIC_CONTEXT_LABELS.en;
const contextBlock = strategicContext?.trim()
? `\n <div class="strategic-context">
<h3>${escapeHtml(contextLabel)}</h3>
<p>${escapeHtml(strategicContext.trim())}</p>
</div>`
: '';
const html = `<section class="stakeholder-swot-analysis" aria-label="${escapeHtml(titleText)}">
<h2>${escapeHtml(titleText)}</h2>
<div class="stakeholder-swot-grid">
${cards}
</div>${contextBlock}
</section>`;
return {
id: 'stakeholder-swot-analysis',
html,
className: 'stakeholder-swot-analysis-section',
};
}
|