@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.
@@ -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.0",
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) of 4-7 nodes
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 `**Slide type:**` annotation if present. Otherwise infer:
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 data suitable for charting | `evidence` | Inline chart (`[chart:name]` in content) or full-viewport `type: "chart"` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztffn/presentation-generator-plugin",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Claude Code plugin for generating graph-based presentations",
5
5
  "bin": {
6
6
  "presentation-generator-plugin": "bin/index.js"
@@ -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+ minutes | 6-7 | 2-3 |
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 4-7 spine nodes. Each can have 1-3 optional drill-down children below it.
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, move to Phase 3.
72
+ When the agent finishes (or after you write the brief yourself), validate the brief before continuing:
73
73
 
74
- If no documents were provided, write _temp/presentation-content-brief.json yourself from the user's prompt, then move to Phase 3.
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 tell the user:
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.