All files / scripts/data-transformers/content-generators mindmap-section.ts

92.85% Statements 39/42
82.92% Branches 34/41
100% Functions 11/11
100% Lines 38/38

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 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327                                                                                                                                                                                                                                              23x                             23x                                 23x                                             3x 3x     5x     3x         2x 2x     3x   4x     3x             4x 4x 4x     4x         4x         88x 88x 88x 88x   88x 88x 3x 85x 46x 113x         88x       88x                                                                                                           59x   59x   59x       59x       59x 88x     59x   59x                       59x            
/**
 * @module data-transformers/content-generators/mindmap-section
 * @description Generates a color-coded mindmap HTML section using pure CSS —
 * no JavaScript or third-party libraries required.
 *
 * The mindmap renders a central topic node surrounded by color-coded branch
 * nodes. Each branch can have child leaf items, AI-weighted items, and
 * stakeholder sub-branches for hierarchical depth (2–3 levels).
 *
 * Supports AI-driven conceptual mapping with:
 * - Central thesis display (AI-synthesized statement)
 * - Weighted item visualization (critical / significant / moderate / minor)
 * - Stakeholder sub-branches on primary branches
 * - Cross-branch connection indicators
 * - ARIA list role for screen reader accessibility
 *
 * Typical usage: inject into deep-inspection articles to visualise the
 * relationship between a focus topic and detected policy domains, parliamentary
 * actors, data sources (CIA, World Bank, SCB), or legislative outcomes.
 *
 * Agentic workflows append the returned `TemplateSection` to `ArticleData.sections`.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import { escapeHtml } from '../../html-utils.js';
import type { Language } from '../../types/language.js';
import type { TemplateSection } from '../../types/article.js';
 
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
 
/** Pre-defined semantic color roles for mindmap branches */
export type MindmapBranchColor =
  | 'cyan'     // primary topic / key actors
  | 'magenta'  // threats / risks / opposition
  | 'yellow'   // opportunities / future
  | 'green'    // strengths / positive outcomes
  | 'purple'   // data sources / background
  | 'orange'   // legislative pipeline / process
  | 'blue'     // international / EU context
  | 'red';     // urgency / critical issues
 
/** Political dimension for AI-driven mindmap branches */
export type MindmapDimension = 'power' | 'impact' | 'timeline' | 'scope' | 'motivation';
 
/** Relative political significance weight for AI-driven mindmap items */
export type AIMindmapItemWeight = 'critical' | 'significant' | 'moderate' | 'minor';
 
/** A single AI-weighted item within a mindmap branch */
export interface AIMindmapItem {
  /** Display text for the item */
  text: string;
  /** Relative political significance of this item */
  weight: AIMindmapItemWeight;
  /** Optional AI-generated reasoning for why this item matters */
  aiReasoning?: string;
}
 
/** A stakeholder-perspective sub-branch within a primary branch */
export interface SubBranch {
  /** Sub-branch label (e.g., stakeholder name) */
  label: string;
  /** Child items in this sub-branch */
  items?: string[];
}
 
/** A cross-branch connection indicator */
export interface MindmapConnection {
  /** Label of the source branch */
  fromBranch: string;
  /** Label of the target branch */
  toBranch: string;
  /** AI-described relationship between the two branches */
  relationship: string;
}
 
/** A single branch of the mindmap, attached to the central node */
export interface MindmapBranch {
  /** Branch label (rendered in the colored branch node) */
  label: string;
  /** Semantic color for the branch node */
  color: MindmapBranchColor;
  /** Child leaf items displayed below the branch node (plain text) */
  items?: string[];
  /** Optional icon/emoji prefix for the branch label */
  icon?: string;
  /** AI-weighted items (displayed instead of plain `items` when provided) */
  aiItems?: AIMindmapItem[];
  /** Stakeholder sub-branches for hierarchical depth */
  subBranches?: SubBranch[];
  /** Political dimension this branch represents */
  dimension?: MindmapDimension;
}
 
/** Options for the mindmap section generator */
export interface MindmapSectionOptions {
  /** Central topic text (the root of the mindmap) */
  topic: string;
  /** Array of branches radiating from the central node */
  branches: MindmapBranch[];
  /** Target language for section labels */
  lang: Language | string;
  /** Optional section title override */
  title?: string;
  /** Optional introductory paragraph rendered above the mindmap */
  summary?: string;
  /** AI-generated thesis statement displayed in the central node */
  centralThesis?: string;
  /** Cross-branch connection indicators */
  connections?: MindmapConnection[];
}
 
// ---------------------------------------------------------------------------
// Color palette  (matches cyberpunk theme CSS variables from styles.css)
// ---------------------------------------------------------------------------
 
const BRANCH_COLORS: Readonly<Record<MindmapBranchColor, { bg: string; border: string; text: string }>> = {
  cyan:    { bg: '#0a2a33', border: '#00d9ff', text: '#00d9ff' },
  magenta: { bg: '#2a0a1a', border: '#ff006e', text: '#ff006e' },
  yellow:  { bg: '#2a200a', border: '#ffbe0b', text: '#ffbe0b' },
  green:   { bg: '#0a2a0a', border: '#83cf39', text: '#83cf39' },
  purple:  { bg: '#1a0a2a', border: '#9d4edd', text: '#9d4edd' },
  orange:  { bg: '#2a1500', border: '#f77f00', text: '#f77f00' },
  blue:    { bg: '#0a1230', border: '#4895ef', text: '#4895ef' },
  red:     { bg: '#2a0a0a', border: '#e63946', text: '#e63946' },
};
 
// ---------------------------------------------------------------------------
// Section title labels (14 languages)
// ---------------------------------------------------------------------------
 
const SECTION_TITLES: Partial<Record<string, string>> = {
  en: 'Policy Mindmap',
  sv: 'Policykarta',
  da: 'Politikkort',
  no: 'Politikkart',
  fi: 'Politiikkakartta',
  de: 'Politikkarte',
  fr: 'Carte conceptuelle',
  es: 'Mapa conceptual',
  nl: 'Beleidskaart',
  ar: 'خريطة السياسات',
  he: 'מפת מדיניות',
  ja: '政策マインドマップ',
  ko: '정책 마인드맵',
  zh: '政策思维导图',
};
 
const CONNECTIONS_ARIA_LABELS: Partial<Record<string, string>> = {
  en: 'Cross-branch connections',
  sv: 'Grenarnas kopplingar',
  da: 'Grenkoblinger',
  no: 'Grenforbindelser',
  fi: 'Haarojen yhteydet',
  de: 'Zweigverbindungen',
  fr: 'Connexions entre branches',
  es: 'Conexiones entre ramas',
  nl: 'Verbindingen tussen takken',
  ar: 'الروابط بين الفروع',
  he: 'קשרים בין ענפים',
  ja: 'ブランチ間の接続',
  ko: '브랜치 간 연결',
  zh: '分支间连接',
};
 
// ---------------------------------------------------------------------------
// Rendering helpers
// ---------------------------------------------------------------------------
 
/** Render AI-weighted items as a styled list */
function renderAIItems(items: AIMindmapItem[]): string {
  Iif (items.length === 0) return '';
  const listItems = items
    .map(
      item =>
        `        <li class="mindmap-ai-item" role="listitem" data-weight="${escapeHtml(item.weight)}">${escapeHtml(item.text)}</li>`,
    )
    .join('\n');
  return `\n      <ul class="mindmap-leaf-list mindmap-ai-list" role="list">\n${listItems}\n      </ul>`;
}
 
/** Render stakeholder sub-branches with nested items */
function renderSubBranches(subBranches: SubBranch[]): string {
  Iif (subBranches.length === 0) return '';
  return subBranches
    .map(sb => {
      const subItems =
        sb.items && sb.items.length > 0
          ? `\n          <ul class="mindmap-sub-items" role="list">\n${sb.items
              .map(i => `            <li role="listitem">${escapeHtml(i)}</li>`)
              .join('\n')}\n          </ul>`
          : '';
      return `      <div class="mindmap-sub-branch">\n        <div class="mindmap-sub-branch-label">${escapeHtml(sb.label)}</div>${subItems}\n      </div>`;
    })
    .join('\n');
}
 
/** Render cross-branch connection indicators */
function renderConnections(connections: MindmapConnection[], lang: Language | string): string {
  Iif (connections.length === 0) return '';
  const ariaLabel = CONNECTIONS_ARIA_LABELS[lang as string] ?? CONNECTIONS_ARIA_LABELS.en!;
  const items = connections
    .map(
      c =>
        `    <div class="mindmap-connection" role="listitem" data-from="${escapeHtml(c.fromBranch)}" data-to="${escapeHtml(c.toBranch)}">` +
        `↔ ${escapeHtml(c.fromBranch)} ↔ ${escapeHtml(c.toBranch)}: ${escapeHtml(c.relationship)}` +
        `</div>`,
    )
    .join('\n');
  return `  <div class="mindmap-connections" aria-label="${escapeHtml(ariaLabel)}" role="list">\n${items}\n  </div>\n`;
}
 
/** Render a single branch node with its leaf items, AI items, and sub-branches */
function renderBranch(branch: MindmapBranch): string {
  const palette = BRANCH_COLORS[branch.color] ?? BRANCH_COLORS.cyan;
  const iconPrefix = branch.icon ? `${branch.icon} ` : '';
  const labelHtml = `${escapeHtml(iconPrefix)}${escapeHtml(branch.label)}`;
  const dimAttr = branch.dimension ? ` data-dimension="${escapeHtml(branch.dimension)}"` : '';
 
  let contentHtml = '';
  if (branch.aiItems && branch.aiItems.length > 0) {
    contentHtml = renderAIItems(branch.aiItems);
  } else if (branch.items && branch.items.length > 0) {
    contentHtml = `\n      <ul class="mindmap-leaf-list" role="list">\n${branch.items
      .map(item => `        <li role="listitem">${escapeHtml(item)}</li>`)
      .join('\n')}\n      </ul>`;
  }
 
  const subBranchesHtml =
    branch.subBranches && branch.subBranches.length > 0
      ? `\n      <div class="mindmap-sub-branches">\n${renderSubBranches(branch.subBranches)}\n      </div>`
      : '';
 
  return `    <div class="mindmap-branch" role="listitem"${dimAttr}
      style="--branch-bg:${palette.bg};--branch-border:${palette.border};--branch-text:${palette.text}">
      <div class="mindmap-branch-label">${labelHtml}</div>${contentHtml}${subBranchesHtml}
    </div>`;
}
 
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
 
/**
 * Generate a color-coded mindmap section.
 *
 * Returns a `TemplateSection` (pure HTML/CSS — no JavaScript) that can be
 * appended to `ArticleData.sections`. The mindmap renders a central topic node
 * surrounded by colored branch nodes, each optionally containing child items,
 * AI-weighted items, and stakeholder sub-branches.
 *
 * Supports AI-driven conceptual mapping features:
 * - `centralThesis` — AI-synthesized statement in the center node
 * - `connections` — Cross-branch connection indicators
 * - `MindmapBranch.aiItems` — Weighted items with `data-weight` attribute
 * - `MindmapBranch.subBranches` — Stakeholder sub-branches (hierarchical depth)
 *
 * The CSS for `.mindmap-section` lives in `styles/components/mindmap.css`
 * (imported by `styles.css`). No client-side JS is required or loaded.
 *
 * @example
 * ```ts
 * const section = generateMindmapSection({
 *   topic: 'Cybersecurity Policy',
 *   lang: 'en',
 *   centralThesis: 'Parliamentary focus on cybersecurity spans defensive legislation and EU alignment.',
 *   branches: [
 *     {
 *       label: 'Key Actors',
 *       color: 'cyan',
 *       icon: '👥',
 *       dimension: 'power',
 *       aiItems: [
 *         { text: 'Ministry of Defence', weight: 'critical' },
 *         { text: 'NCSC', weight: 'significant' },
 *       ],
 *       subBranches: [
 *         { label: 'Government', items: ['Policy initiative', 'Regulatory mandate'] },
 *         { label: 'Opposition', items: ['Oversight function', 'Amendment proposals'] },
 *       ],
 *     },
 *   ],
 * });
 * articleData.sections = [...(articleData.sections ?? []), section];
 * ```
 */
export function generateMindmapSection(opts: MindmapSectionOptions): TemplateSection {
  const { topic, branches, centralThesis, connections } = opts;
 
  const titleText = opts.title?.trim() || SECTION_TITLES[opts.lang as string] || SECTION_TITLES.en!;
 
  const summaryBlock = opts.summary?.trim()
    ? `  <p class="mindmap-summary">${escapeHtml(opts.summary.trim())}</p>\n`
    : '';
 
  const thesisHtml = centralThesis?.trim()
    ? `\n    <p class="mindmap-thesis">${escapeHtml(centralThesis.trim())}</p>`
    : '';
 
  const branchCount = branches.length;
  const branchItems = branches.map(b => renderBranch(b)).join('\n');
 
  const connectionsHtml =
    connections && connections.length > 0 ? renderConnections(connections, opts.lang) : '';
 
  const html = `<section class="mindmap-section" aria-label="${escapeHtml(titleText)}">
  <h2>${escapeHtml(titleText)}</h2>
${summaryBlock}  <div class="mindmap-container" data-branch-count="${branchCount}">
    <div class="mindmap-center-wrapper">
      <div class="mindmap-center" role="heading" aria-level="3">${escapeHtml(topic)}</div>${thesisHtml}
    </div>
    <div class="mindmap-branches" role="list" aria-label="${escapeHtml(titleText)}">
${branchItems}
    </div>
  </div>
${connectionsHtml}</section>`;
 
  return {
    id: 'mindmap-section',
    html,
    className: 'mindmap-section-wrapper',
  };
}