@ztffn/presentation-generator-plugin 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/presentation-narrative.md +5 -1
- package/hooks/hooks.json +10 -0
- package/hooks/inject-plugin-root.sh +32 -0
- package/package.json +1 -1
- package/scripts/outline_to_graph.py +29 -0
- package/skills/frameworks/SKILL.md +2 -1
- package/skills/graph-topology/SKILL.md +1 -1
- package/skills/presentation-generator/SKILL.md +41 -9
- package/skills/upload-presentation/SKILL.md +155 -0
- package/DEFERRED-FIXES.md +0 -38
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "presentation-generator",
|
|
3
3
|
"description": "Generate complete graph-based presentations from natural language briefs and project documents. Pipeline: content extraction, narrative design, deterministic graph generation, and visual styling.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Huma"
|
|
7
7
|
},
|
|
@@ -32,7 +32,7 @@ Read `_temp/presentation-content-brief.json`. Note any weak or missing fields.
|
|
|
32
32
|
### Step 3 — Select framework, design structure, write slide content
|
|
33
33
|
|
|
34
34
|
Based on `goal` and `audience`, select the appropriate narrative framework from your frameworks skill. Then:
|
|
35
|
-
1. Design the spine (main horizontal flow)
|
|
35
|
+
1. Design the spine (main horizontal flow) — use the duration-based spine length table in your frameworks skill to determine node count
|
|
36
36
|
2. Design drill-down branches where content warrants optional depth
|
|
37
37
|
3. Write detailed content for each slide following the quality guidance in your slide-content skill
|
|
38
38
|
|
|
@@ -55,6 +55,10 @@ Write a markdown file to `_temp/presentation-outline.md`. The format is a hard c
|
|
|
55
55
|
- CONTENT headers for spine: `### SPINE N: Title`
|
|
56
56
|
- CONTENT headers for drill-downs: `### SPINE N.M: Title`
|
|
57
57
|
|
|
58
|
+
**Two formats that look reasonable but silently break the parser:**
|
|
59
|
+
- `- **Io Thermal Storage: The foundation** — Phase-change iron...` ✗ — title must not be inside the bold markers; the N.M index must be
|
|
60
|
+
- `### Io Thermal Storage: The foundation` / `**(Drill-down under Spine Node 2)**` ✗ — no freeform title headers; must be `### SPINE 2.1: Io Thermal Storage`
|
|
61
|
+
|
|
58
62
|
```markdown
|
|
59
63
|
# Presentation Outline: [Deck Title]
|
|
60
64
|
|
package/hooks/hooks.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# inject-plugin-root.sh — PostToolUse hook for Skill tool.
|
|
3
|
+
# Fires after the presentation-generator skill loads and injects
|
|
4
|
+
# PLUGIN_ROOT into additionalContext so the orchestrator never needs bash.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
SKILL=$(echo "$INPUT" | jq -r '.tool_input.skill // .tool_input.name // empty')
|
|
8
|
+
|
|
9
|
+
# Only run for the presentation-generator skill
|
|
10
|
+
case "$SKILL" in
|
|
11
|
+
*presentation-generator*) ;;
|
|
12
|
+
*) exit 0 ;;
|
|
13
|
+
esac
|
|
14
|
+
|
|
15
|
+
# Locate the plugin root
|
|
16
|
+
PLUGIN_ROOT=$(find -L .claude ~/.claude \
|
|
17
|
+
-path "*/presentation-generator/scripts/outline_to_graph.py" \
|
|
18
|
+
2>/dev/null | head -1 | sed 's|/scripts/outline_to_graph.py||')
|
|
19
|
+
|
|
20
|
+
[ -z "$PLUGIN_ROOT" ] && exit 0
|
|
21
|
+
|
|
22
|
+
CONTEXT="PLUGIN_ROOT: $PLUGIN_ROOT"
|
|
23
|
+
ESCAPED=$(echo "$CONTEXT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
|
24
|
+
|
|
25
|
+
cat <<EOF
|
|
26
|
+
{
|
|
27
|
+
"hookSpecificOutput": {
|
|
28
|
+
"hookEventName": "PostToolUse",
|
|
29
|
+
"additionalContext": $ESCAPED
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
EOF
|
package/package.json
CHANGED
|
@@ -93,6 +93,18 @@ def parse_drilldowns(text):
|
|
|
93
93
|
minor = _letter.get(dd_letter.group(1).upper(), 1)
|
|
94
94
|
title = dd_letter.group(2).strip()
|
|
95
95
|
drilldowns[current_parent].append((current_parent, minor, title))
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# Fallback C: "- **Title**" or "- **Title: desc**" or "- **Title — desc**"
|
|
99
|
+
# Agent wrote title-first bold instead of "- **N.M** Title:". Extract the
|
|
100
|
+
# title portion (before any colon, em-dash, or en-dash) and auto-number.
|
|
101
|
+
bold_title = re.match(r"-\s+\*\*([^*]+?)\*\*", line)
|
|
102
|
+
if bold_title:
|
|
103
|
+
raw = bold_title.group(1)
|
|
104
|
+
title = re.split(r"[:\u2014\u2013]", raw)[0].strip()
|
|
105
|
+
if title:
|
|
106
|
+
minor = len(drilldowns[current_parent]) + 1
|
|
107
|
+
drilldowns[current_parent].append((current_parent, minor, title))
|
|
96
108
|
|
|
97
109
|
return drilldowns
|
|
98
110
|
|
|
@@ -131,6 +143,23 @@ def parse_content_blocks(text):
|
|
|
131
143
|
flags=re.MULTILINE,
|
|
132
144
|
)
|
|
133
145
|
|
|
146
|
+
# Normalize freeform drill-down headers:
|
|
147
|
+
# ### Some Title
|
|
148
|
+
# **(Drill-down under Spine Node N ...)**
|
|
149
|
+
# → ### SPINE N.M: Some Title (M auto-incremented per parent)
|
|
150
|
+
_dd_minor: dict = {}
|
|
151
|
+
def _normalize_freeform_dd(m):
|
|
152
|
+
title = m.group(1).strip()
|
|
153
|
+
parent = int(m.group(2))
|
|
154
|
+
_dd_minor[parent] = _dd_minor.get(parent, 0) + 1
|
|
155
|
+
return f"### SPINE {parent}.{_dd_minor[parent]}: {title}"
|
|
156
|
+
text = re.sub(
|
|
157
|
+
r"^### (.+?)\n\*\*\(?Drill-down under Spine(?:\s+Node)?\s+(\d+)[^)]*\)?\*\*",
|
|
158
|
+
_normalize_freeform_dd,
|
|
159
|
+
text,
|
|
160
|
+
flags=re.MULTILINE,
|
|
161
|
+
)
|
|
162
|
+
|
|
134
163
|
blocks = {}
|
|
135
164
|
parts = re.split(r"(?=^### SPINE )", text, flags=re.MULTILINE)
|
|
136
165
|
|
|
@@ -137,7 +137,8 @@ Frameworks define the spine structure. Drill-downs can use a different pattern:
|
|
|
137
137
|
|---|---|---|
|
|
138
138
|
| 5-10 minutes | 4-5 | 0-1 |
|
|
139
139
|
| 15-20 minutes | 5-6 | 1-2 |
|
|
140
|
-
| 30
|
|
140
|
+
| 30-45 minutes | 7-9 | 2-4 |
|
|
141
|
+
| 60-90 minutes | 8-10 | 4-7 |
|
|
141
142
|
|
|
142
143
|
The spine is the minimum viable presentation. Drill-downs are optional depth the presenter can use or skip based on time and audience interest.
|
|
143
144
|
|
|
@@ -35,7 +35,7 @@ No drill-downs. Every slide is on the main horizontal path.
|
|
|
35
35
|
|
|
36
36
|
### 2. Spine with Deep Dives
|
|
37
37
|
|
|
38
|
-
Main path has
|
|
38
|
+
Main path has spine nodes scaled to presentation duration (see frameworks skill for the duration table). Each spine node can have optional drill-down children below it — longer presentations go deeper rather than wider.
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
[Cover] → [Problem] → [Solution] → [Value] → [CTA]
|
|
@@ -15,16 +15,24 @@ You are an orchestrator. Execute these 7 phases in order. Do not skip phases. Do
|
|
|
15
15
|
|
|
16
16
|
## Phase 1 — Setup
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
**Locate the plugin root.**
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
First, check if your context already contains a line beginning with `PLUGIN_ROOT:`
|
|
21
|
+
(injected automatically by a hook when running in Claude Code). If found, use that
|
|
22
|
+
value directly — you are done with this step.
|
|
23
|
+
|
|
24
|
+
If not found (Cowork mode, or hook not active), use the **Read tool** to probe
|
|
25
|
+
these paths in order — stop at the first one that returns file contents:
|
|
26
|
+
|
|
27
|
+
1. `.claude/plugins/presentation-generator/scripts/outline_to_graph.py`
|
|
28
|
+
2. `~/.claude/plugins/presentation-generator/scripts/outline_to_graph.py`
|
|
29
|
+
|
|
30
|
+
PLUGIN_ROOT is everything before `/scripts/outline_to_graph.py` in the successful
|
|
31
|
+
path (e.g. `.claude/plugins/presentation-generator`).
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
If the output is empty, the plugin is not installed — stop and tell the user.
|
|
33
|
+
If neither Read succeeds, stop and tell the user the plugin is not installed.
|
|
26
34
|
|
|
27
|
-
Save
|
|
35
|
+
Save PLUGIN_ROOT — you need it for every later phase.
|
|
28
36
|
|
|
29
37
|
Then extract from the user's message:
|
|
30
38
|
- **TOPIC**: what is being presented
|
|
@@ -184,12 +192,36 @@ cp _temp/presentation-content-brief.json "presentations/{SLUG}/content-brief.jso
|
|
|
184
192
|
cp _temp/presentation-outline.md "presentations/{SLUG}/outline.md"
|
|
185
193
|
```
|
|
186
194
|
|
|
187
|
-
Then
|
|
195
|
+
Then upload the presentation automatically. Read the upload skill first:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
Read: {PLUGIN_ROOT}/skills/upload-presentation/SKILL.md
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The presentation JSON is at `presentations/{SLUG}/{SLUG}.json`. The `name` field must be at the JSON root — use the TOPIC from Phase 1:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
jq --arg name "{TOPIC}" '. + {"name": $name}' "presentations/{SLUG}/{SLUG}.json" | \
|
|
205
|
+
curl -s -X POST "https://humagreenfield.netlify.app/api/presentations/upload" \
|
|
206
|
+
-H "Content-Type: application/json" \
|
|
207
|
+
-H "X-API-Key: TheSunIsFree" \
|
|
208
|
+
-d @-
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
On success, report the returned URLs to the user:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
Presentation published:
|
|
215
|
+
- Present: https://humagreenfield.netlify.app{presentUrl}
|
|
216
|
+
- Edit: https://humagreenfield.netlify.app{editUrl}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
If the upload fails, fall back to manual import:
|
|
188
220
|
|
|
189
221
|
```
|
|
190
222
|
Your presentation is at: presentations/{SLUG}/{SLUG}.json
|
|
191
223
|
|
|
192
|
-
To import:
|
|
224
|
+
To import manually:
|
|
193
225
|
1. Open https://humagreenfield.netlify.app/present/plan
|
|
194
226
|
2. New presentation → Import JSON
|
|
195
227
|
3. Select the file above
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: upload-presentation
|
|
3
|
+
description: >
|
|
4
|
+
Upload a React Flow presentation graph JSON directly to the Huma Showcase platform via HTTP API.
|
|
5
|
+
Use when an agent has generated a presentation (e.g. from the presentation-generator skill) and
|
|
6
|
+
wants to publish it without manual browser interaction. Triggers on: "upload the presentation",
|
|
7
|
+
"publish the deck", "deploy the slides", "push to the showcase", "upload presentation-draft.json".
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Upload Presentation
|
|
11
|
+
|
|
12
|
+
Publishes a presentation graph (nodes + edges) to the platform via a single HTTP call.
|
|
13
|
+
Returns a live URL — no browser interaction needed.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- API key: `TheSunIsFree`
|
|
18
|
+
- The JSON to upload must match the `GraphPayload` shape: `{ name, nodes[], edges[] }`.
|
|
19
|
+
|
|
20
|
+
Pass the key via the `X-API-Key` header or `?key=TheSunIsFree` query param.
|
|
21
|
+
|
|
22
|
+
## Upload Command
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
curl -s -X POST "https://humagreenfield.netlify.app/api/presentations/upload" \
|
|
26
|
+
-H "Content-Type: application/json" \
|
|
27
|
+
-H "X-API-Key: TheSunIsFree" \
|
|
28
|
+
-d @_temp/presentation-draft.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or with the key in the URL:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -s -X POST "https://humagreenfield.netlify.app/api/presentations/upload?key=TheSunIsFree" \
|
|
35
|
+
-H "Content-Type: application/json" \
|
|
36
|
+
-d @_temp/presentation-draft.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Request Body
|
|
40
|
+
|
|
41
|
+
The file at `_temp/presentation-draft.json` must contain:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"name": "Human-readable presentation title",
|
|
46
|
+
"description": "Optional one-line description",
|
|
47
|
+
"nodes": [ /* GraphNode[] — see node schema below */ ],
|
|
48
|
+
"edges": [ /* GraphEdge[] */ ]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The API requires `name` at the JSON root. Supply it explicitly using the TOPIC from Phase 1:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
jq --arg name "{TOPIC}" '. + {"name": $name}' "presentations/{SLUG}/{SLUG}.json" | \
|
|
56
|
+
curl -s -X POST "https://humagreenfield.netlify.app/api/presentations/upload" \
|
|
57
|
+
-H "Content-Type: application/json" \
|
|
58
|
+
-H "X-API-Key: TheSunIsFree" \
|
|
59
|
+
-d @-
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If uploading a manually constructed payload, ensure `nodes` is present and non-empty (`[]` is accepted but produces a blank presentation).
|
|
63
|
+
|
|
64
|
+
### Minimal node shape
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"id": "cover",
|
|
69
|
+
"type": "huma",
|
|
70
|
+
"position": { "x": 0, "y": 0 },
|
|
71
|
+
"data": {
|
|
72
|
+
"label": "Slide Title",
|
|
73
|
+
"content": "Optional markdown body",
|
|
74
|
+
"notes": "Optional speaker notes"
|
|
75
|
+
},
|
|
76
|
+
"style": { "width": 180, "height": 70 },
|
|
77
|
+
"measured": { "width": 180, "height": 70 }
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Full field reference: `.claude/skills/presentation-generator/system/node-schema.md`
|
|
82
|
+
|
|
83
|
+
### Minimal edge shape
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"id": "cover-to-problem",
|
|
88
|
+
"source": "cover",
|
|
89
|
+
"target": "problem",
|
|
90
|
+
"sourceHandle": "s-right",
|
|
91
|
+
"targetHandle": "t-left"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Handle pairs: `s-right/t-left` (→), `s-left/t-right` (←), `s-bottom/t-top` (↓), `s-top/t-bottom` (↑)
|
|
96
|
+
|
|
97
|
+
## Response
|
|
98
|
+
|
|
99
|
+
Success (`200`):
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"ok": true,
|
|
104
|
+
"slug": "q1-strategy",
|
|
105
|
+
"name": "Q1 Strategy",
|
|
106
|
+
"presentUrl": "/present/q1-strategy",
|
|
107
|
+
"editUrl": "/present/plan/q1-strategy"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Prepend the site origin to get absolute URLs:
|
|
112
|
+
- Present: `https://humagreenfield.netlify.app/present/q1-strategy`
|
|
113
|
+
- Edit: `https://humagreenfield.netlify.app/present/plan/q1-strategy`
|
|
114
|
+
|
|
115
|
+
Error responses use standard HTTP status codes with `{ "error": "..." }` body:
|
|
116
|
+
|
|
117
|
+
| Status | `error` message | Fix |
|
|
118
|
+
|--------|----------------|-----|
|
|
119
|
+
| `400` | `"Invalid JSON body"` | Body is not valid JSON |
|
|
120
|
+
| `400` | `"Body must be a JSON object"` | Wrap payload in `{}` |
|
|
121
|
+
| `400` | `"name is required (string)"` | Add `"name"` field to the JSON root |
|
|
122
|
+
| `400` | `"nodes is required (array)"` | Add `"nodes": [...]` to the JSON root |
|
|
123
|
+
| `401` | `"Unauthorized"` | Check API key — must be `TheSunIsFree` |
|
|
124
|
+
| `500` | `"Server misconfiguration: PRESENTATIONS_API_KEY is not set"` | Env var missing on server |
|
|
125
|
+
| `500` | Storage error message | Blob write failed — retry or check Netlify status |
|
|
126
|
+
|
|
127
|
+
## Slug Behavior
|
|
128
|
+
|
|
129
|
+
The slug is derived from `name` via `slugify()` (lowercase, hyphens, max 64 chars).
|
|
130
|
+
If the slug already exists, a numeric suffix is appended automatically (`-2`, `-3`, …).
|
|
131
|
+
Bundled template slugs (`lyse-neo-pitch`, `presentation-demo`, `huma-partner-pitch`) are always protected.
|
|
132
|
+
|
|
133
|
+
## Integration with Presentation Generator
|
|
134
|
+
|
|
135
|
+
In Phase 7 of the `presentation-generator` pipeline, the output JSON is at `presentations/{SLUG}/{SLUG}.json`. Upload it with the TOPIC as the name:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
jq --arg name "{TOPIC}" '. + {"name": $name}' "presentations/{SLUG}/{SLUG}.json" | \
|
|
139
|
+
curl -s -X POST "https://humagreenfield.netlify.app/api/presentations/upload" \
|
|
140
|
+
-H "Content-Type: application/json" \
|
|
141
|
+
-H "X-API-Key: TheSunIsFree" \
|
|
142
|
+
-d @-
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Local Dev
|
|
146
|
+
|
|
147
|
+
Replace the Netlify URL with `http://localhost:3000`. The endpoint behaves identically — blobs
|
|
148
|
+
fall back to `_temp/react-flow/presentations/` on the local filesystem.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
curl -s -X POST "http://localhost:3000/api/presentations/upload" \
|
|
152
|
+
-H "Content-Type: application/json" \
|
|
153
|
+
-H "X-API-Key: TheSunIsFree" \
|
|
154
|
+
-d @_temp/presentation-draft.json
|
|
155
|
+
```
|
package/DEFERRED-FIXES.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Deferred Fixes
|
|
2
|
-
|
|
3
|
-
Issues that require architectural changes and should be addressed in targeted, focused fixes rather than prompt-level patches.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## v1.5.0 fixes ✓ resolved
|
|
8
|
-
|
|
9
|
-
**Bug: Style agent always launched as `undefined`**
|
|
10
|
-
`enforce-style-schema.sh` returned `updatedInput: { prompt: "..." }` which replaced
|
|
11
|
-
the entire tool_input, dropping `subagent_type`. Fixed by merging the modified prompt
|
|
12
|
-
into the original tool_input: `updatedInput: ($orig + {prompt: $prompt})`.
|
|
13
|
-
|
|
14
|
-
**Bug: Content brief triggered presentation validator**
|
|
15
|
-
Hook patterns `*_temp/presentation*.json` matched `_temp/presentation-content-brief.json`,
|
|
16
|
-
causing the validator to reject it. Removed the broad arm — only `*_temp/presentation-draft.json`
|
|
17
|
-
needs validation now.
|
|
18
|
-
|
|
19
|
-
**Bug: Bare agent names caused first-attempt failures**
|
|
20
|
-
SKILL.md used `presentation-content` etc; Task tool requires fully-qualified
|
|
21
|
-
`presentation-generator:presentation-content`. Fixed in all three phases (2, 3, 6).
|
|
22
|
-
|
|
23
|
-
**UX: Visual intent annotations leaking into slide content**
|
|
24
|
-
Narrative agents sometimes write `**Visual intent:** bookend` inside `**Content:**` blocks.
|
|
25
|
-
Added `**Visual intent:**` as a recognized field marker in `extract_fields()` so it acts
|
|
26
|
-
as a boundary, preventing bleed into `data.content`. Documented in `outline-format/SKILL.md`.
|
|
27
|
-
|
|
28
|
-
**UX: Intimidating inline scripts in permission prompts**
|
|
29
|
-
Phase 1 multi-line if/elif replaced with a single `find` one-liner.
|
|
30
|
-
Phase 5 Python heredoc extracted into `scripts/verify_node_count.py`.
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Issue 3 — `topic` badge truncation ✓ resolved
|
|
35
|
-
|
|
36
|
-
**Was:** Badge clips at ~180px wide when `topic` strings are long.
|
|
37
|
-
|
|
38
|
-
**Resolution:** The root cause was slide titles being written at full descriptive length (10–15 words) rather than as sharp 6–10 word claims. Fixed by adding slide title length rules to `slide-content/SKILL.md` (narrative agent) and reinforcing the hard limit in the style agent's label rewriting rules. Short titles produce short `topic` strings naturally — no schema change or UI change required.
|