@ztffn/presentation-generator-plugin 1.6.0 → 1.6.2
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/agents/presentation-style.md +2 -2
- package/package.json +1 -1
- package/scripts/outline_to_graph.py +29 -0
- package/scripts/validate_content_brief.py +203 -0
- package/skills/frameworks/SKILL.md +2 -1
- package/skills/graph-topology/SKILL.md +1 -1
- package/skills/presentation-generator/SKILL.md +34 -4
- package/skills/slide-content/SKILL.md +2 -0
- package/skills/slide-recipes/SKILL.md +17 -0
- package/skills/upload-presentation/SKILL.md +155 -0
- package/DEFERRED-FIXES.md +0 -50
|
@@ -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.6.
|
|
4
|
+
"version": "1.6.2",
|
|
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
|
|
|
@@ -30,7 +30,7 @@ These define the visual intent mapping, slide recipes, allowed data fields, and
|
|
|
30
30
|
|
|
31
31
|
### Step 3 — Classify each slide
|
|
32
32
|
|
|
33
|
-
Walk through every node and assign a visual intent. Use the outline's `**
|
|
33
|
+
Walk through every node and assign a visual intent. Use the outline's `**Visual intent:**` annotation if present. Otherwise infer:
|
|
34
34
|
|
|
35
35
|
| Position / Content Signal | Visual Intent | Treatment |
|
|
36
36
|
|---|---|---|
|
|
@@ -40,7 +40,7 @@ Walk through every node and assign a visual intent. Use the outline's `**Slide t
|
|
|
40
40
|
| Spine node with a single bold claim | `impact` | Centered, no bullets, whitespace-forward |
|
|
41
41
|
| Spine or drill-down with 4+ bullet points | `workhorse` | Left-aligned, `centered: false`, single or two-column |
|
|
42
42
|
| Slide with comparison data (before/after, us/them, old/new) | `evidence` | Two-column layout, split on `---` |
|
|
43
|
-
| Slide with numeric
|
|
43
|
+
| Slide with 2+ numeric values of the same type, or named items scored across options | `evidence` | **Synthesize a chart** — do not leave this as bullets. Add `charts` dict, embed `[chart:name]` in content, remove raw data from bullet form. Use chart-trigger signals in your slide-recipes skill to select chart type. |
|
|
44
44
|
| Slide after 2+ consecutive bullet-heavy slides | `breathing-room` | Centered, background image, minimal text |
|
|
45
45
|
| Drill-down with detailed specs or numbers | `workhorse` or `evidence` | Depends on whether data is comparative or sequential |
|
|
46
46
|
|
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
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate_content_brief.py — Validates the content brief JSON produced by Phase 2.
|
|
4
|
+
Checks required fields, detects placeholder/weak values, and warns on thin content.
|
|
5
|
+
Exit 0 if brief is usable; exit 1 if critically malformed.
|
|
6
|
+
Usage: python3 validate_content_brief.py <path-to-content-brief.json>
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
VALID_GOALS = {"pitch", "demo", "update", "internal", "exploratory"}
|
|
15
|
+
|
|
16
|
+
# Sentinel strings the content agent writes when data is absent in source material.
|
|
17
|
+
NOT_FOUND_RE = re.compile(r"NOT FOUND IN SOURCE", re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
# Generic/weak phrases that should not appear as values — adapted from content-signals SKILL.md.
|
|
20
|
+
WEAK_VALUE_RE = re.compile(
|
|
21
|
+
r"\b(significant|various|many|some|multiple|considerable|substantial|good|better|great|saves time|easy to use|improves efficiency)\b",
|
|
22
|
+
re.IGNORECASE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# A specific number means the string contains at least one digit sequence.
|
|
26
|
+
HAS_NUMBER_RE = re.compile(r"\d")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_not_found(value):
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
return bool(NOT_FOUND_RE.search(value))
|
|
32
|
+
if isinstance(value, list):
|
|
33
|
+
return all(isinstance(v, str) and NOT_FOUND_RE.search(v) for v in value) and len(value) > 0
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_string(field, value, errors, warnings):
|
|
38
|
+
"""Validate a required top-level string field."""
|
|
39
|
+
if not isinstance(value, str) or not value.strip():
|
|
40
|
+
errors.append(f"{field}: missing or empty")
|
|
41
|
+
return
|
|
42
|
+
if is_not_found(value):
|
|
43
|
+
warnings.append(f"{field}: contains 'NOT FOUND IN SOURCE' placeholder — brief is incomplete")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate(path):
|
|
47
|
+
errors = []
|
|
48
|
+
warnings = []
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
raw = Path(path).read_text()
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
print(f"ERROR File not found: {path}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
brief = json.loads(raw)
|
|
58
|
+
except json.JSONDecodeError as e:
|
|
59
|
+
print(f"ERROR Invalid JSON: {e}")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
if not isinstance(brief, dict):
|
|
63
|
+
print("ERROR Content brief must be a JSON object")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# --- product ---
|
|
67
|
+
product = brief.get("product")
|
|
68
|
+
if not isinstance(product, dict):
|
|
69
|
+
errors.append("product: missing or not an object")
|
|
70
|
+
else:
|
|
71
|
+
if not str(product.get("name", "")).strip():
|
|
72
|
+
errors.append("product.name: missing or empty")
|
|
73
|
+
if not str(product.get("description", "")).strip():
|
|
74
|
+
errors.append("product.description: missing or empty")
|
|
75
|
+
|
|
76
|
+
# --- audience ---
|
|
77
|
+
audience = brief.get("audience")
|
|
78
|
+
if not isinstance(audience, dict):
|
|
79
|
+
errors.append("audience: missing or not an object")
|
|
80
|
+
else:
|
|
81
|
+
for sub in ("who", "background", "cares_about"):
|
|
82
|
+
val = audience.get(sub, "")
|
|
83
|
+
if not isinstance(val, str) or not val.strip():
|
|
84
|
+
errors.append(f"audience.{sub}: missing or empty")
|
|
85
|
+
elif is_not_found(val):
|
|
86
|
+
warnings.append(f"audience.{sub}: 'NOT FOUND IN SOURCE' placeholder — narrative will lack audience specificity")
|
|
87
|
+
|
|
88
|
+
# --- goal ---
|
|
89
|
+
goal = brief.get("goal", "")
|
|
90
|
+
if not isinstance(goal, str) or not goal.strip():
|
|
91
|
+
errors.append("goal: missing or empty — must be one of: pitch, demo, update, internal, exploratory")
|
|
92
|
+
elif goal not in VALID_GOALS:
|
|
93
|
+
if is_not_found(goal):
|
|
94
|
+
warnings.append("goal: 'NOT FOUND IN SOURCE' placeholder — framework selection will be generic")
|
|
95
|
+
else:
|
|
96
|
+
errors.append(f"goal: '{goal}' is not a valid value — must be one of: {', '.join(sorted(VALID_GOALS))}")
|
|
97
|
+
|
|
98
|
+
# --- keyMessages ---
|
|
99
|
+
key_messages = brief.get("keyMessages")
|
|
100
|
+
if not isinstance(key_messages, list) or len(key_messages) == 0:
|
|
101
|
+
errors.append("keyMessages: missing or empty array — at least one declarative sentence required")
|
|
102
|
+
else:
|
|
103
|
+
real = [m for m in key_messages if isinstance(m, str) and not NOT_FOUND_RE.search(m) and m.strip()]
|
|
104
|
+
if len(real) == 0:
|
|
105
|
+
errors.append("keyMessages: all entries are placeholders — no real key messages extracted")
|
|
106
|
+
elif len(real) < 3:
|
|
107
|
+
warnings.append(f"keyMessages: only {len(real)} non-placeholder entry(ies) — schema requires 3–5 declarative sentences")
|
|
108
|
+
elif len(real) > 5:
|
|
109
|
+
warnings.append(f"keyMessages: {len(real)} entries — schema recommends 3–5")
|
|
110
|
+
for i, msg in enumerate(real):
|
|
111
|
+
if WEAK_VALUE_RE.search(msg):
|
|
112
|
+
warnings.append(f"keyMessages[{i}]: weak/generic language detected — '{msg[:80]}...' — make it specific")
|
|
113
|
+
|
|
114
|
+
# --- valueProps ---
|
|
115
|
+
value_props = brief.get("valueProps")
|
|
116
|
+
if not isinstance(value_props, list) or len(value_props) == 0:
|
|
117
|
+
warnings.append("valueProps: missing or empty — narrative may lack audience-specific benefit framing")
|
|
118
|
+
elif is_not_found(value_props):
|
|
119
|
+
warnings.append("valueProps: all entries are placeholders — will produce generic benefit statements")
|
|
120
|
+
|
|
121
|
+
# --- dataPoints ---
|
|
122
|
+
data_points = brief.get("dataPoints")
|
|
123
|
+
if not isinstance(data_points, list) or len(data_points) == 0:
|
|
124
|
+
errors.append("dataPoints: missing or empty — at least one specific metric is required")
|
|
125
|
+
else:
|
|
126
|
+
has_specific_number = False
|
|
127
|
+
for i, dp in enumerate(data_points):
|
|
128
|
+
if not isinstance(dp, dict):
|
|
129
|
+
errors.append(f"dataPoints[{i}]: must be an object with label, value, source")
|
|
130
|
+
continue
|
|
131
|
+
val = str(dp.get("value", ""))
|
|
132
|
+
if HAS_NUMBER_RE.search(val) and not NOT_FOUND_RE.search(val):
|
|
133
|
+
has_specific_number = True
|
|
134
|
+
else:
|
|
135
|
+
warnings.append(
|
|
136
|
+
f"dataPoints[{i}] ('{dp.get('label', '?')}'): value '{val}' has no specific number — "
|
|
137
|
+
f"weak metric (schema requires a concrete figure)"
|
|
138
|
+
)
|
|
139
|
+
if not dp.get("source", "").strip():
|
|
140
|
+
warnings.append(f"dataPoints[{i}] ('{dp.get('label', '?')}'): missing source attribution")
|
|
141
|
+
if not has_specific_number:
|
|
142
|
+
errors.append("dataPoints: no entry has a specific numeric value — brief cannot support evidence slides")
|
|
143
|
+
|
|
144
|
+
# --- proofPoints ---
|
|
145
|
+
proof_points = brief.get("proofPoints")
|
|
146
|
+
if not isinstance(proof_points, list) or len(proof_points) == 0:
|
|
147
|
+
warnings.append("proofPoints: missing or empty — narrative will lack named evidence")
|
|
148
|
+
elif is_not_found(proof_points):
|
|
149
|
+
warnings.append("proofPoints: all entries are placeholders")
|
|
150
|
+
|
|
151
|
+
# --- objections ---
|
|
152
|
+
objections = brief.get("objections")
|
|
153
|
+
if not isinstance(objections, list) or len(objections) == 0:
|
|
154
|
+
warnings.append("objections: missing or empty — narrative cannot preempt audience concerns")
|
|
155
|
+
elif is_not_found(objections):
|
|
156
|
+
warnings.append("objections: all entries are placeholders")
|
|
157
|
+
|
|
158
|
+
# --- callToAction ---
|
|
159
|
+
cta = brief.get("callToAction", "")
|
|
160
|
+
check_string("callToAction", cta, errors, warnings)
|
|
161
|
+
if isinstance(cta, str) and cta.strip() and not is_not_found(cta):
|
|
162
|
+
# Heuristic: a real CTA contains an action verb and a concrete outcome
|
|
163
|
+
weak_cta_patterns = re.compile(
|
|
164
|
+
r"^(let'?s|we should|continue|follow up|stay in touch|keep talking)",
|
|
165
|
+
re.IGNORECASE,
|
|
166
|
+
)
|
|
167
|
+
if weak_cta_patterns.match(cta.strip()):
|
|
168
|
+
warnings.append(f"callToAction: '{cta}' — too vague; should name a specific action and outcome")
|
|
169
|
+
|
|
170
|
+
# --- context ---
|
|
171
|
+
context = brief.get("context", "")
|
|
172
|
+
if not isinstance(context, str) or not context.strip():
|
|
173
|
+
warnings.append("context: missing or empty — narrative agent has no competitive or timing signals")
|
|
174
|
+
elif is_not_found(context):
|
|
175
|
+
warnings.append("context: 'NOT FOUND IN SOURCE' placeholder")
|
|
176
|
+
|
|
177
|
+
# --- Report ---
|
|
178
|
+
if warnings:
|
|
179
|
+
print(f"WARN {len(warnings)} warning(s):")
|
|
180
|
+
for w in warnings:
|
|
181
|
+
print(f" WARN {w}")
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
if errors:
|
|
185
|
+
for e in errors:
|
|
186
|
+
print(f"ERROR {e}")
|
|
187
|
+
print()
|
|
188
|
+
print(f"FAILED {len(errors)} critical error(s) — content brief is not usable; re-run Phase 2")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
if not warnings:
|
|
192
|
+
print("OK Content brief is valid")
|
|
193
|
+
else:
|
|
194
|
+
print(f"OK (with warnings) Content brief is usable — {len(warnings)} weak field(s) noted above")
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
if len(sys.argv) != 2:
|
|
200
|
+
print("Usage: python3 validate_content_brief.py <path-to-content-brief.json>")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
ok = validate(sys.argv[1])
|
|
203
|
+
sys.exit(0 if ok else 1)
|
|
@@ -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]
|
|
@@ -69,9 +69,15 @@ Write a JSON content brief to _temp/presentation-content-brief.json
|
|
|
69
69
|
Report back what you extracted.
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
When the agent finishes,
|
|
72
|
+
When the agent finishes (or after you write the brief yourself), validate the brief before continuing:
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
```bash
|
|
75
|
+
python3 "{PLUGIN_ROOT}/scripts/validate_content_brief.py" _temp/presentation-content-brief.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If exit code is non-zero, stop and show the errors to the user — do not proceed to Phase 3.
|
|
79
|
+
|
|
80
|
+
If no documents were provided, write _temp/presentation-content-brief.json yourself from the user's prompt, then run the validation above.
|
|
75
81
|
|
|
76
82
|
---
|
|
77
83
|
|
|
@@ -192,12 +198,36 @@ cp _temp/presentation-content-brief.json "presentations/{SLUG}/content-brief.jso
|
|
|
192
198
|
cp _temp/presentation-outline.md "presentations/{SLUG}/outline.md"
|
|
193
199
|
```
|
|
194
200
|
|
|
195
|
-
Then
|
|
201
|
+
Then upload the presentation automatically. Read the upload skill first:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
Read: {PLUGIN_ROOT}/skills/upload-presentation/SKILL.md
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The presentation JSON is at `presentations/{SLUG}/{SLUG}.json`. The `name` field must be at the JSON root — use the TOPIC from Phase 1:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
jq --arg name "{TOPIC}" '. + {"name": $name}' "presentations/{SLUG}/{SLUG}.json" | \
|
|
211
|
+
curl -s -X POST "https://humagreenfield.netlify.app/api/presentations/upload" \
|
|
212
|
+
-H "Content-Type: application/json" \
|
|
213
|
+
-H "X-API-Key: TheSunIsFree" \
|
|
214
|
+
-d @-
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
On success, report the returned URLs to the user:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
Presentation published:
|
|
221
|
+
- Present: https://humagreenfield.netlify.app{presentUrl}
|
|
222
|
+
- Edit: https://humagreenfield.netlify.app{editUrl}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
If the upload fails, fall back to manual import:
|
|
196
226
|
|
|
197
227
|
```
|
|
198
228
|
Your presentation is at: presentations/{SLUG}/{SLUG}.json
|
|
199
229
|
|
|
200
|
-
To import:
|
|
230
|
+
To import manually:
|
|
201
231
|
1. Open https://humagreenfield.netlify.app/present/plan
|
|
202
232
|
2. New presentation → Import JSON
|
|
203
233
|
3. Select the file above
|
|
@@ -261,4 +261,6 @@ Example annotation in an outline (use `### SPINE N:` header format — the parse
|
|
|
261
261
|
|
|
262
262
|
5. **Breathing room after sustained density.** If the outline has a run of `workhorse` and `evidence` slides (detailed argument, data, comparison), follow it with a `breathing-room` or `impact` slide before continuing. The audience needs a reset.
|
|
263
263
|
|
|
264
|
+
6. **Chart rhythm.** One `evidence` slide per major section is a signal. Two in a row is a pattern the audience can follow. Three or more consecutive `evidence` slides is a data dump — it exhausts attention and flattens the argument. If the content demands multiple data points, prefer one chart slide with a strong claim followed by a drill-down for the supporting data, rather than stacking chart slides on the spine. Equally, a deck with no `evidence` at any point reads as assertion without proof — aim for at least one per major argument.
|
|
265
|
+
|
|
264
266
|
These annotations describe intent only. How the design agent translates them into layout, background, and typography is not the narrative agent's concern.
|
|
@@ -112,6 +112,23 @@ Set `showBranding: false` for immersive slides (R3F, full-bleed video).
|
|
|
112
112
|
|
|
113
113
|
When a signal is present: synthesise the `charts` dict from the outline figures, embed `[chart:name]` in the content markdown, and remove the raw data from bullet form. The agent must extract values from the outline text — do not leave chart-ready data as bullets.
|
|
114
114
|
|
|
115
|
+
**Data extraction and creation rules:**
|
|
116
|
+
|
|
117
|
+
The outline rarely contains a tidy data table. More often, values are embedded in prose or bullets. The agent must parse them out and construct the data array. When exact values are missing or incomplete, synthesise plausible values consistent with the narrative — a chart that conveys the right order of magnitude and relative relationship is more useful than no chart.
|
|
118
|
+
|
|
119
|
+
| Outline text | What to do |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `- Iron oxide: 3,000 kJ/kg` / `- Lithium-ion: 576 kJ/kg` / `- Molten salt: 360 kJ/kg` | Extract label + value pairs → `[{material, energy_density}]`, `xKey: "material"`, `yKeys: ["energy_density"]`, chart type: `bar` |
|
|
122
|
+
| `Response time down 60%, cost down 40% over 6 months` | Construct time-series or before/after → `[{period: "Before", response_ms: 100, cost: 100}, {period: "After", response_ms: 40, cost: 60}]`, chart type: `bar` |
|
|
123
|
+
| `Outperforms competitors on flexibility, integration speed, and reliability` | Named items across multiple axes → `[{axis: "Flexibility", ours: 90, competitor: 35}, ...]`, chart type: `radar` |
|
|
124
|
+
| `Revenue grew 40% QoQ` — no breakdown given | Create a minimal 2-point bar: `[{quarter: "Q2", revenue: 100}, {quarter: "Q3", revenue: 140}]`. Label it clearly. |
|
|
125
|
+
|
|
126
|
+
Key principles:
|
|
127
|
+
- Use short, readable axis labels — `"Q3"` not `"Third Quarter of Fiscal Year"`
|
|
128
|
+
- `yKeys` values become legend labels — use the words the audience would say: `"ours"` / `"competitor"`, `"before"` / `"after"`, `"target"` / `"actual"`
|
|
129
|
+
- Normalise to a common base (e.g., index to 100) when absolute values would mislead
|
|
130
|
+
- When data is qualitative (e.g. "best-in-class"), skip the chart — use an impact or workhorse slide instead
|
|
131
|
+
|
|
115
132
|
Chart placement:
|
|
116
133
|
- Data IS the primary message of the slide → `type: "chart"` (full-viewport)
|
|
117
134
|
- Data supports a text argument → `[chart:name]` inline in a `two-column` layout
|
|
@@ -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,50 +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.6.0 fixes ✓ resolved
|
|
8
|
-
|
|
9
|
-
**UX: Phase 1 bash command visible in permission dialog**
|
|
10
|
-
The Phase 1 `find` one-liner (shipped in v1.5.0) still appeared as a bash
|
|
11
|
-
permission prompt. Added `inject-plugin-root.sh` — a `PostToolUse` hook on the
|
|
12
|
-
`Skill` tool that silently resolves PLUGIN_ROOT and injects it as
|
|
13
|
-
`additionalContext` when the orchestrator skill loads. Phase 1 now reads the
|
|
14
|
-
injected value from context with no bash at all. Cowork fallback uses two
|
|
15
|
-
ordered `Read` probes of known install paths — also no bash.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## v1.5.0 fixes ✓ resolved
|
|
20
|
-
|
|
21
|
-
**Bug: Style agent always launched as `undefined`**
|
|
22
|
-
`enforce-style-schema.sh` returned `updatedInput: { prompt: "..." }` which replaced
|
|
23
|
-
the entire tool_input, dropping `subagent_type`. Fixed by merging the modified prompt
|
|
24
|
-
into the original tool_input: `updatedInput: ($orig + {prompt: $prompt})`.
|
|
25
|
-
|
|
26
|
-
**Bug: Content brief triggered presentation validator**
|
|
27
|
-
Hook patterns `*_temp/presentation*.json` matched `_temp/presentation-content-brief.json`,
|
|
28
|
-
causing the validator to reject it. Removed the broad arm — only `*_temp/presentation-draft.json`
|
|
29
|
-
needs validation now.
|
|
30
|
-
|
|
31
|
-
**Bug: Bare agent names caused first-attempt failures**
|
|
32
|
-
SKILL.md used `presentation-content` etc; Task tool requires fully-qualified
|
|
33
|
-
`presentation-generator:presentation-content`. Fixed in all three phases (2, 3, 6).
|
|
34
|
-
|
|
35
|
-
**UX: Visual intent annotations leaking into slide content**
|
|
36
|
-
Narrative agents sometimes write `**Visual intent:** bookend` inside `**Content:**` blocks.
|
|
37
|
-
Added `**Visual intent:**` as a recognized field marker in `extract_fields()` so it acts
|
|
38
|
-
as a boundary, preventing bleed into `data.content`. Documented in `outline-format/SKILL.md`.
|
|
39
|
-
|
|
40
|
-
**UX: Intimidating inline scripts in permission prompts**
|
|
41
|
-
Phase 1 multi-line if/elif replaced with a single `find` one-liner.
|
|
42
|
-
Phase 5 Python heredoc extracted into `scripts/verify_node_count.py`.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## Issue 3 — `topic` badge truncation ✓ resolved
|
|
47
|
-
|
|
48
|
-
**Was:** Badge clips at ~180px wide when `topic` strings are long.
|
|
49
|
-
|
|
50
|
-
**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.
|