# Vision API

Base URL: `https://vision.ctrl1.com`

All request bodies use `Content-Type: application/json`.

## Workflow

1. `POST /api/render` with `deck` (JSON slide array or JSX string).
2. `GET` the `url` from the render response to download `deck.pptx`.

---

## POST /api/render

Render a deck to PowerPoint and return a public download URL.

### Request body

| Field | Type | Description |
|-------|------|-------------|
| `deck` | array or string | **Required.** JSON slide array or JSX string (see below) |
| `medium` | string | Optional, default `"ppt"` |
| `packId` | string | Optional, default `"smedi"`. Only `"smedi"` is supported. |

### Response

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | UUID — render job id |
| `packId` | string | Pack id |
| `medium` | string | Output medium |
| `fileName` | string | Always `deck.pptx` |
| `url` | string | Absolute URL to download the file |
| `durationMs` | number | Server-side render time in milliseconds |

### Response example

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "packId": "smedi",
  "medium": "ppt",
  "fileName": "deck.pptx",
  "url": "https://vision.ctrl1.com/output/api/smedi/ppt/550e8400-e29b-41d4-a716-446655440000/deck.pptx",
  "durationMs": 842
}
```

Download the file with `GET` on `url` (no request body).

### Request examples

JSON deck:

```js
fetch('https://vision.ctrl1.com/api/render', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    packId: 'smedi',
    deck: [
      { type: 'SectionDivider', props: { sectionNumber: '01', title: 'API Test' } },
    ],
  }),
});
```

JSX deck:

```js
fetch('https://vision.ctrl1.com/api/render', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    packId: 'smedi',
    deck: '<Cover title="Hello" subtitle="World" />',
  }),
});
```

---

## Deck formats

### JSON

```json
[
  { "type": "Cover", "props": { "title": "Title", "subtitle": "Subtitle", "author": "Name", "date": "2026-05" } },
  { "type": "TOC", "props": { "title": "目 录", "items": [{ "label": "Chapter 1", "number": "1" }] } },
  { "type": "SectionDivider", "props": { "sectionNumber": "1", "title": "Section" } },
  { "type": "ThankYou", "props": { "title": "感谢观看", "footer": "" } }
]
```

### JSX

A JSX fragment or module, e.g. `<Cover title="Hello" />` or multiple roots. With `packId: "smedi"`, components map to fixed PPT templates — **only pass business content** (titles, items, body text). Do **not** pass `*SourceControlId`, `bodyArchetype`, or `slideNumberSourceControlId` on L1 (those are bound in the pack).

Deck structure:

- **Pages** — one component = one slide (`Cover`, `SectionDivider`, `ThankYou`); `TOC` may expand to **multiple slides** when `items.length > 5`.
- **Blocks** — body layout only; **must** be wrapped in exactly one `<Content>` child (see below).

---

### Pages

Standalone slide types. Each renders a full page with layout chrome (footer bar, logo, slide number where applicable).

#### `Cover`

Cover / title slide (smedi slide 1).

| Prop | Type | Description |
|------|------|-------------|
| `title` | string | Main title |
| `subtitle` | string | Subtitle (under title) |
| `author` | string | Presenter line |
| `date` | string | Date line |
| `footer` | string | Optional footer |

```jsx
<Cover
  title="上海市政总院PPT模板"
  subtitle="科研课题申报"
  author="汇报人：XXX"
  date="汇报日期：XXXX年X月"
/>
```

#### `TOC`

Table of contents (smedi slide 2).

| Prop | Type | Description |
|------|------|-------------|
| `title` | string | Heading (default `"目 录"`) |
| `items` | array | `{ label, number }` per row; **5 rows per slide** — more than 5 items auto-splits into multiple TOC pages |
| `itemsDirection` | string | Optional, default `"column"` |

```jsx
<TOC
  title="目 录"
  items={[
    { label: '项目背景及意义', number: '1' },
    { label: '项目研究内容', number: '2' },
    { label: '项目研究方案', number: '3' },
    { label: '项目研究计划', number: '4' },
    { label: '项目研究基础', number: '5' },
  ]}
/>
```

#### `SectionDivider`

Chapter divider (smedi slide 3).

| Prop | Type | Description |
|------|------|-------------|
| `sectionNumber` | string | Chapter index in the number panel (e.g. `"1"`) |
| `title` | string | Section heading |

```jsx
<SectionDivider sectionNumber="1" title="项目背景及意义" />
```

#### `ThankYou`

Closing slide (smedi slide 42).

| Prop | Type | Description |
|------|------|-------------|
| `title` | string | Main line (default `"感谢观看"`) |
| `footer` | string | Optional footer / slogan |
| `message` | string | Alias for `title` |

```jsx
<ThankYou title="感谢观看" footer="" />
```

#### `Content`

**Content page shell** — not used alone. Wrap **exactly one** block child. Put the **page title** on `Content`; the pack infers PPTX shell (title control, slide number, body region) from the block type and optional `blockRole`.

| Prop | Type | Description |
|------|------|-------------|
| `title` | string | **Required** for standard content slides — page heading |
| `body` | string | Rare; only for legacy shells that use inline body on Content |
| `items` | array | Do not use on Content — pass `items` on the child block |

```jsx
<Content title="2.4技术关键">
  <PillLabelColumns items={[...]} />
</Content>
```

**Rules**

- One block per `Content` (no siblings).
- Do not nest `Content` inside `Content`.
- Blocks listed below are the supported body types for API decks.

---

### Blocks (inside `Content` only)

Body components for layout-001 content slides. **Always:**

```jsx
<Content title="页标题">
  <BlockComponent ... />
</Content>
```

Fixed slot counts: extra `items` reuse the last template control and break layout. Machine-readable contracts: `packages/semantic/block-catalog.js`.

#### `FramedQuoteBody`

Framed panel + quote decoration (smedi slide 4). Page title on `Content`; **no `items` array** — pass body text only.

| Prop | Type | Description |
|------|------|-------------|
| `body` | string | Quote / background body text inside the frame |

```jsx
<Content title="1.1课题背景">
  <FramedQuoteBody body="点击输入文本内容" />
</Content>
```

#### `SubtitleBody`

Secondary heading + body paragraph (smedi slide 43). Page title on `Content`; subtitle and body live on the block.

| Prop | Type | Description |
|------|------|-------------|
| `subtitle` | string | Secondary heading below the page title |
| `body` | string | Main body paragraph |

```jsx
<Content title="基础页展示">
  <SubtitleBody
    subtitle="模板使用说明"
    body="模板围绕总院标志性元素设计…"
  />
</Content>
```

#### `PillLabelColumns`

Pill-label columns (smedi slide 24). **Column count follows `items.length`** — equal-width auto layout (3-column template; 4+ supported).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | `{ label, value }` per column — **1～N 列等宽自动排版** |
| `itemsDirection` | string | Optional, default `"row"` |

```jsx
<Content title="2.4技术关键">
  <PillLabelColumns
    items={[
      { label: '小标题', value: '点击输入文本内容' },
      { label: '小标题', value: '点击输入文本内容' },
      { label: '小标题', value: '点击输入文本内容' },
    ]}
  />
</Content>
```

#### `Timeline`

Horizontal **four-phase** timeline (smedi slide 6). Page title on `Content`.

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **4** entries: `{ label, value }` per phase |
| `itemsDirection` | string | Optional, default `"row"` |

```jsx
<Content title="1.2国内外研究现状">
  <Timeline
    items={[
      { label: '阶段1', value: '说明' },
      { label: '阶段2', value: '说明' },
      { label: '阶段3', value: '说明' },
      { label: '阶段4', value: '说明' },
    ]}
    itemsDirection="row"
  />
</Content>
```

#### `ProjectRoadmap` (slide 040)

Six-phase **project roadmap** with staircase diagram + colored milestone markers (smedi slide 40). Page title on `Content`; **fixed 6 steps** (`items.length` must be 6). Not the four-phase `Timeline` (slide 6).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **6** entries: `{ label, value }` — `label` is subheading; `value` is body |

```jsx
<Content title="5.4近期项目计划">
  <ProjectRoadmap
    items={[
      { label: '小标题', value: '点击输入文本' },
      { label: '小标题', value: '点击输入文本' },
      { label: '小标题', value: '点击输入文本' },
      { label: '小标题', value: '点击输入文本' },
      { label: '小标题', value: '点击输入文本' },
      { label: '小标题', value: '点击输入文本' },
    ]}
  />
</Content>
```

Deprecated: `Timeline` + `blockRole="six-phase-project-roadmap"` still expands to `ProjectRoadmap`.

#### `ProjectInnovationPoints` (slide 031)

2×2 **project innovation points** grid — colored circles, white-outline star icons, and label/body rows (smedi slide 31). Page title on `Content`; **fixed 4 points** (`items.length` must be 4).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **4** entries: `{ label, value }` — `label` is point heading; `value` is body |

```jsx
<Content title="3.1项目创新点">
  <ProjectInnovationPoints
    items={[
      { label: '创新点', value: '点击输入文本内容' },
      { label: '创新点', value: '点击输入文本内容' },
      { label: '创新点', value: '点击输入文本内容' },
      { label: '创新点', value: '点击输入文本内容' },
    ]}
  />
</Content>
```

Deprecated: `InnovationPointQuad` still expands to `ProjectInnovationPoints`.

#### `ProjectChallengeStrategy` (slide 032)

Three stacked **project challenge / strategy** rows — PNG row bands + left/right text panels and center step badge (smedi slide 32). Page title on `Content`; **fixed 3 rows** (`items.length` must be 3).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **3** entries: `{ step, challengeLabel, challengeBody, strategyLabel, strategyBody }` — `step` is `01`–`03`; left column is challenge, right is strategy |

```jsx
<Content title="3.1项目难点及应对策略">
  <ProjectChallengeStrategy
    items={[
      {
        step: '01',
        challengeLabel: '难点',
        challengeBody: '点击输入文本内容',
        strategyLabel: '应对策略',
        strategyBody: '点击输入文本内容',
      },
      {
        step: '02',
        challengeLabel: '难点',
        challengeBody: '点击输入文本内容',
        strategyLabel: '应对策略',
        strategyBody: '点击输入文本内容',
      },
      {
        step: '03',
        challengeLabel: '难点',
        challengeBody: '点击输入文本内容',
        strategyLabel: '应对策略',
        strategyBody: '点击输入文本内容',
      },
    ]}
  />
</Content>
```

Deprecated: `ChallengeStrategyColumns` still expands to `ProjectChallengeStrategy`.

#### `VsComparison`

Left / right dual panel + center diagram (smedi slide 7). **No `items` array.**

| Prop | Type | Description |
|------|------|-------------|
| `leftLabel` | string | Left panel heading |
| `rightLabel` | string | Right panel heading |
| `leftBody` | string | Left body text |
| `rightBody` | string | Right body text |

```jsx
<Content title="1.2国内外研究现状">
  <VsComparison
    leftLabel="国内"
    rightLabel="国外"
    leftBody="点击输入文本"
    rightBody="点击输入文本"
  />
</Content>
```

#### `AccentCardQuad`

2×2 accent cards (smedi slide 25).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **4** entries: `{ label, value }` (`label` often empty) |

```jsx
<Content title="2.5预期效果">
  <AccentCardQuad
    items={[
      { label: '', value: '点击输入文本' },
      { label: '', value: '点击输入文本' },
      { label: '', value: '点击输入文本' },
      { label: '', value: '点击输入文本' },
    ]}
  />
</Content>
```

#### `CircleStepRow`

Four overlapping gradient circles + shared footer caption (smedi slide 19). Page title on `Content`; each circle is a vector ellipse with two centered lines (number + subtitle).

| Prop | Type | Description |
|------|------|-------------|
| `body` | string | Footer caption below the circle row |
| `items` | array | **4** entries: `{ label, value }` — `label` is the step number (e.g. `01`); `value` is the subtitle inside the circle |

```jsx
<Content title="2.1研究目标">
  <CircleStepRow
    body="点击输入文本内容 点击输入文本内容 点击输入文本内容 点击输入文本内容"
    items={[
      { label: '01', value: '数字化' },
      { label: '02', value: '海量化' },
      { label: '03', value: '透明化' },
      { label: '04', value: '便捷化' },
    ]}
  />
</Content>
```

#### `HexagonStepRow`

Lead paragraph + four staggered hex steps (smedi slide 16). Page title on `Content`; left accent bar + lead text, then four hex frames each with step number and body.

| Prop | Type | Description |
|------|------|-------------|
| `lead` | string | Intro paragraph beside the accent bar |
| `items` | array | **4** entries: `{ label, value }` — `label` is the step number (e.g. `01`); `value` is the step body |

```jsx
<Content title="1.3选题意义">
  <HexagonStepRow
    lead="点击输入文本内容"
    items={[
      { label: '01', value: '点击输入文本' },
      { label: '02', value: '点击输入文本' },
      { label: '03', value: '点击输入文本' },
      { label: '04', value: '点击输入文本' },
    ]}
  />
</Content>
```

#### `CycleHubNotes`

Six-segment cycle diagram + three notes on each flank (smedi slide 20). Page title on `Content`; `centerNote` is an editable text box at the hub center; `items` are left column (first 3) then right column (last 3).

| Prop | Type | Description |
|------|------|-------------|
| `centerNote` | string | Center hub label inside the cycle diagram |
| `items` | array | **6** entries: `{ label, value }` — `label` often empty; `value` is flank note body |

```jsx
<Content title="2.2研究内容">
  <CycleHubNotes
    centerNote="此图示 仅为示例"
    items={[
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
    ]}
  />
</Content>
```

#### `ImplementationMethodFlow` (slide 037)

Three gear shapes with numbered hub labels and flank body text — **项目实施方法** layout (smedi slide 37). Page title on `Content`; **fixed 3 steps** (`items.length` must be 3). Not the linear chevron `ProcessFlowDiagram` (slide 022).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **3** entries: `{ label, value }` — `label` is step number (`01`–`03`); `value` is flank body |
| `itemsDirection` | string | Optional layout hint; default `row` |

```jsx
<Content title="5.1项目实施方法">
  <ImplementationMethodFlow
    items={[
      { label: '01', value: '点击输入文本内容' },
      { label: '02', value: '点击输入文本内容' },
      { label: '03', value: '点击输入文本内容' },
    ]}
  />
</Content>
```

#### `PyramidLevelNotes`

Four-tier pyramid diagram + four level caption boxes (smedi slide 29). Page title on `Content`; left tower is a pack image asset; right captions use progressively darker borders (top→bottom).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **4** entries: `{ label, value }` — `label` often empty; `value` is the caption body |

```jsx
<Content title="2.5预期效果">
  <PyramidLevelNotes
    items={[
      { label: '', value: '点击输入文本' },
      { label: '', value: '点击输入文本' },
      { label: '', value: '点击输入文本' },
      { label: '', value: '点击输入文本' },
    ]}
  />
</Content>
```

#### `SimpleCardRow`

Five-column icon + title + description cards (smedi slide 48). **Column count follows `items.length`.**

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | `{ title, description, iconSrc? }` per card — **1～N 列等宽自动排版** |

```jsx
<Content title="核心优势">
  <SimpleCardRow
    items={[
      { title: '完全本地化部署', description: '模型推理本地完成…' },
      { title: '多模型智能调度', description: '支持 Llama/GPT/Claude/Gemini…' },
      { title: '智能体架构先进', description: '内置12大核心功能模块…' },
      { title: '技能系统可扩展', description: '18个专业技能模块…' },
      { title: '企业级稳定可靠', description: '7×24小时不间断运行…' },
    ]}
  />
</Content>
```

#### `StaggeredImagePair`

Two-column staggered image + text (smedi slide 26). **No `items` array.**

| Prop | Type | Description |
|------|------|-------------|
| `leftImage` | string | Left column image caption (placeholder label) |
| `leftSrc` | string | Left column bitmap: URL, `data:image/...`, or raw base64 |
| `leftBody` | string | Left column body |
| `rightBody` | string | Right column body |
| `rightImage` | string | Right column image caption |
| `rightSrc` | string | Right column bitmap |

```jsx
<Content title="2.5预期效果">
  <StaggeredImagePair
    leftImage="图片1"
    leftBody="文本"
    rightBody="文本"
    rightImage="图片2"
  />
</Content>
```

#### `StaggeredImageQuad`

Four- or six-cell image / chart grids — **one L1 component**, `blockRole` picks the template.

| Variant | Items | `blockRole` | `image` field meaning |
|---------|-------|---------------|------------------------|
| Staggered columns (slide 27) | **4** | omit (default) | Image label + `caption` body |
| Image placeholders (slide 41) | **4** | `four-image-placeholder-grid` | 图片01… + caption |
| Photo style grid (slide 44) | **4** | `four-photo-style-grid` | Style name (样式1…) |
| Chart showcase (slide 45) | **6** | `six-chart-type-grid` | Chart type label |

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **4 or 6** entries: `{ image, caption, src? }` — `src` is bitmap URL or base64 |
| `itemsDirection` | string | Optional, default `"row"` |
| `blockRole` | string | See table above |

```jsx
<Content title="5.4近期项目计划">
  <StaggeredImageQuad
    blockRole="four-image-placeholder-grid"
    items={[
      { image: '图片01', caption: '01 点击输入图片描述' },
      { image: '图片02', caption: '02 点击输入图片描述' },
      { image: '图片03', caption: '03 点击输入图片描述' },
      { image: '图片04', caption: '04 点击输入图片描述' },
    ]}
  />
</Content>
```

#### `ImageCaptionRows`

Two aligned image + caption rows (smedi slide 28).

| Prop | Type | Description |
|------|------|-------------|
| `items` | array | **2** entries: `{ image, caption, src? }` |

```jsx
<Content title="2.5预期效果">
  <ImageCaptionRows
    items={[
      { image: '图片1', caption: '文本一' },
      { image: '图片2', caption: '文本二' },
    ]}
  />
</Content>
```

#### `MemberDutyTable`

Member duty matrix table (smedi slide 35). **No `items` array** — use `rows`.

| Prop | Type | Description |
|------|------|-------------|
| `rows` | string[][] | **10 × 5** cells: row 0 = header (`岗位`, `人员`, `单位`, `分工`, `岗位职责`), rows 1–9 = data |

```jsx
<Content title="4.2项目成员职责">
  <MemberDutyTable
    rows={[
      ['岗位', '人员', '单位', '分工', '岗位职责'],
      ['项目经理', '张三', '市政总院', '总体协调', '负责项目统筹…'],
      // … 10 rows total
    ]}
  />
</Content>
```

---

### Chart insight pages (core)

**One page = one chart + one insight.** Page title on `Content`; PPTX control ids are fixed in pack expand — do **not** pass `*SourceControlId`. Chart **data** (`series`, `title`, or `imageAsset`) is passed on L1 (`chart` / `leftChart` / `rightChart`); pack skin supplies layout/colors.

| Component | slide | Chart | Props |
|-----------|-------|-------|-------|
| `ChartBarInsight` | 008 | bar | `chart`, `sectionTitle`, `body`, `flip?` |
| `ChartRadarInsight` | 012 | radar | `chart`, `sectionTitle`, `body`, `flip?` |
| `ChartAreaInsight` | 009 | area | `chart`, `body` |
| `ChartScatterInsight` | 013 | scatter | `chart`, `sectionTitle`, `body` |
| `ChartBarLegendInsight` | 014 | bar (horizontal) | `chart`, `items` (**3** legend rows) |

`chart` shape: `{ title?, catAxisTitle?, valAxisTitle?, series?: [{ name, categories, values }], imageAsset? }` — **inline the full object in examples** (do not `import` from repo fixture files).

`flip` (Bar/Radar only): default chart left, insight right; `flip` mirrors columns (slide 008 uses `flip`).

```jsx
<Content title="1.2国内外研究现状">
  <ChartBarInsight
    flip
    chart={{
      title: '图表标题',
      catAxisTitle: '坐标轴标题',
      valAxisTitle: '坐标轴标题',
      series: [
        {
          name: '系列 1',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
        {
          name: '系列 2',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2.4, 4.4, 1.8, 2.8],
        },
        {
          name: '系列 3',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2, 2, 3, 5],
        },
      ],
    }}
    sectionTitle="数据分析"
    body="点击输入文本内容…"
  />
</Content>

<Content title="1.2国内外研究现状">
  <ChartAreaInsight
    chart={{
      title: '图表标题',
      series: [
        {
          name: '系列 1',
          categories: ['类别1', '类别2', '类别3', '类别4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
        {
          name: '系列 2',
          categories: ['类别1', '类别2', '类别3', '类别4'],
          values: [2.4, 4.4, 1.8, 2.8],
        },
        {
          name: '系列 3',
          categories: ['类别1', '类别2', '类别3', '类别4'],
          values: [2, 2, 3, 5],
        },
      ],
    }}
    body="点击输入文本内容…"
  />
</Content>

<Content title="1.2国内外研究现状">
  <ChartBarLegendInsight
    chart={{
      title: '图表标题',
      series: [
        {
          name: '系列 1',
          categories: ['类别1', '类别2', '类别3', '类别4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
        {
          name: '系列 2',
          categories: ['类别1', '类别2', '类别3', '类别4'],
          values: [2.4, 4.4, 1.8, 2.8],
        },
        {
          name: '系列 3',
          categories: ['类别1', '类别2', '类别3', '类别4'],
          values: [2, 2, 3, 5],
        },
      ],
    }}
    items={[
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
      { label: '', value: '点击输入文本内容' },
    ]}
  />
</Content>
```

### Chart compare pages (extended)

Two charts per page — use only when comparing two charts, **not** for one-chart insight pages.

#### `ChartDualPieInsight` (slide 010)

| Prop | Type |
|------|------|
| `leftChart`, `rightChart` | chart data (`series` / `title`) |
| `leftBody`, `rightBody` | string |

#### `ChartDualChartInsight` (slide 011)

| Prop | Type |
|------|------|
| `leftChart`, `rightChart` | chart data (`series` / `title` / `imageAsset`) |
| `leftTitle`, `leftBody`, `rightTitle`, `rightBody` | string |

#### `ChartTripleChartInsight` (slide 021)

Three stacked-bar panels with caption bodies — extended compare page (**not** one-chart-one-insight). Page title on `Content`; pass `series` on L1 (`leftChart` / `centerChart` / `rightChart`); pack skin supplies colors, data-table layout, and control ids.

| Prop | Type |
|------|------|
| `leftChart`, `centerChart`, `rightChart` | chart data (`series`; optional `title`) |
| `leftBody`, `centerBody`, `rightBody` | string |

```jsx
<Content title="2.2研究内容">
  <ChartTripleChartInsight
    leftChart={{
      series: [
        {
          name: '系列 1',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
        {
          name: '系列 2',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [3.4, 2.4, 3.8, 4.8],
        },
        {
          name: '系列 3',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2, 2, 3, 5],
        },
      ],
    }}
    centerChart={{
      series: [
        {
          name: '系列 1',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
        {
          name: '系列 2',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2.4, 4.4, 1.8, 2.8],
        },
        {
          name: '系列 3',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2, 2, 3, 5],
        },
      ],
    }}
    rightChart={{
      series: [
        {
          name: '系列 1',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
        {
          name: '系列 2',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2.4, 4.4, 0.8, 2.8],
        },
        {
          name: '系列 3',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2, 2, 3, 0.1],
        },
      ],
    }}
    leftBody="点击输入文本内容"
    centerBody="点击输入文本内容"
    rightBody="点击输入文本内容"
  />
</Content>
```

```jsx
<Content title="1.2国内外研究现状">
  <ChartDualPieInsight
    leftChart={{
      title: '图表标题',
      series: [
        {
          name: '系列 2',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [2.4, 4.4, 1.8, 2.8],
        },
      ],
    }}
    rightChart={{
      title: '图表标题',
      series: [
        {
          name: '系列 1',
          categories: ['类别 1', '类别 2', '类别 3', '类别 4'],
          values: [4.3, 2.5, 3.5, 4.5],
        },
      ],
    }}
    leftBody="…"
    rightBody="…"
  />
</Content>
```

Deprecated aliases (still expand, prefer Chart*Insight): `MarketAnalysis`, `ChartWithCaption`, `ChartAnalysisStack`, `ChartLegendNotes`, `DualPieCompare`, `DualChartInsight`, `TripleChartInsight`.

---

### Other components

These exist in the semantic layer (`KPIOverview`, `ProcessFlowDiagram`, …) and accept the same `{ type, props }` JSON shape. Prefer documented blocks above for new API decks.

Reference: `packages/semantic/index.jsx`, `packages/semantic/block-catalog.js`.

---

## Errors

Failed requests return **HTTP 400** with:

```json
{ "error": "human-readable message" }
```
