Shared skills now for all agent and fixed the settings for Gunnar and Hermes
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
---
|
||||
name: excalidraw
|
||||
description: "Hand-drawn Excalidraw JSON diagrams (arch, flow, seq)."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
dependencies: []
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [Excalidraw, Diagrams, Flowcharts, Architecture, Visualization, JSON]
|
||||
related_skills: []
|
||||
|
||||
---
|
||||
|
||||
# Excalidraw Diagram Skill
|
||||
|
||||
Create diagrams by writing standard Excalidraw element JSON and saving as `.excalidraw` files. These files can be drag-and-dropped onto [excalidraw.com](https://excalidraw.com) for viewing and editing. No accounts, no API keys, no rendering libraries -- just JSON.
|
||||
|
||||
## When to use
|
||||
|
||||
Generate `.excalidraw` files for architecture diagrams, flowcharts, sequence diagrams, concept maps, and more. Files can be opened at excalidraw.com or uploaded for shareable links.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Load this skill** (you already did)
|
||||
2. **Write the elements JSON** -- an array of Excalidraw element objects
|
||||
3. **Save the file** using `write_file` to create a `.excalidraw` file
|
||||
4. **Optionally upload** for a shareable link using `scripts/upload.py` via `terminal`
|
||||
|
||||
### Saving a Diagram
|
||||
|
||||
Wrap your elements array in the standard `.excalidraw` envelope and save with `write_file`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "hermes-agent",
|
||||
"elements": [ ...your elements array here... ],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Save to any path, e.g. `~/diagrams/my_diagram.excalidraw`.
|
||||
|
||||
### Uploading for a Shareable Link
|
||||
|
||||
Run the upload script (located in this skill's `scripts/` directory) via terminal:
|
||||
|
||||
```bash
|
||||
python skills/diagramming/excalidraw/scripts/upload.py ~/diagrams/my_diagram.excalidraw
|
||||
```
|
||||
|
||||
This uploads to excalidraw.com (no account needed) and prints a shareable URL. Requires the `cryptography` pip package (`pip install cryptography`).
|
||||
|
||||
---
|
||||
|
||||
## Element Format Reference
|
||||
|
||||
### Required Fields (all elements)
|
||||
`type`, `id` (unique string), `x`, `y`, `width`, `height`
|
||||
|
||||
### Defaults (skip these -- they're applied automatically)
|
||||
- `strokeColor`: `"#1e1e1e"`
|
||||
- `backgroundColor`: `"transparent"`
|
||||
- `fillStyle`: `"solid"`
|
||||
- `strokeWidth`: `2`
|
||||
- `roughness`: `1` (hand-drawn look)
|
||||
- `opacity`: `100`
|
||||
|
||||
Canvas background is white.
|
||||
|
||||
### Element Types
|
||||
|
||||
**Rectangle**:
|
||||
```json
|
||||
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 100 }
|
||||
```
|
||||
- `roundness: { "type": 3 }` for rounded corners
|
||||
- `backgroundColor: "#a5d8ff"`, `fillStyle: "solid"` for filled
|
||||
|
||||
**Ellipse**:
|
||||
```json
|
||||
{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }
|
||||
```
|
||||
|
||||
**Diamond**:
|
||||
```json
|
||||
{ "type": "diamond", "id": "d1", "x": 100, "y": 100, "width": 150, "height": 150 }
|
||||
```
|
||||
|
||||
**Labeled shape (container binding)** -- create a text element bound to the shape:
|
||||
|
||||
> **WARNING:** Do NOT use `"label": { "text": "..." }` on shapes. This is NOT a valid
|
||||
> Excalidraw property and will be silently ignored, producing blank shapes. You MUST
|
||||
> use the container binding approach below.
|
||||
|
||||
The shape needs `boundElements` listing the text, and the text needs `containerId` pointing back:
|
||||
```json
|
||||
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 80,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"boundElements": [{ "id": "t_r1", "type": "text" }] },
|
||||
{ "type": "text", "id": "t_r1", "x": 105, "y": 110, "width": 190, "height": 25,
|
||||
"text": "Hello", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e",
|
||||
"textAlign": "center", "verticalAlign": "middle",
|
||||
"containerId": "r1", "originalText": "Hello", "autoResize": true }
|
||||
```
|
||||
- Works on rectangle, ellipse, diamond
|
||||
- Text is auto-centered by Excalidraw when `containerId` is set
|
||||
- The text `x`/`y`/`width`/`height` are approximate -- Excalidraw recalculates them on load
|
||||
- `originalText` should match `text`
|
||||
- Always include `fontFamily: 1` (Virgil/hand-drawn font)
|
||||
|
||||
**Labeled arrow** -- same container binding approach:
|
||||
```json
|
||||
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0,
|
||||
"points": [[0,0],[200,0]], "endArrowhead": "arrow",
|
||||
"boundElements": [{ "id": "t_a1", "type": "text" }] },
|
||||
{ "type": "text", "id": "t_a1", "x": 370, "y": 130, "width": 60, "height": 20,
|
||||
"text": "connects", "fontSize": 16, "fontFamily": 1, "strokeColor": "#1e1e1e",
|
||||
"textAlign": "center", "verticalAlign": "middle",
|
||||
"containerId": "a1", "originalText": "connects", "autoResize": true }
|
||||
```
|
||||
|
||||
**Standalone text** (titles and annotations only -- no container):
|
||||
```json
|
||||
{ "type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello", "fontSize": 20,
|
||||
"fontFamily": 1, "strokeColor": "#1e1e1e", "originalText": "Hello", "autoResize": true }
|
||||
```
|
||||
- `x` is the LEFT edge. To center at position `cx`: `x = cx - (text.length * fontSize * 0.5) / 2`
|
||||
- Do NOT rely on `textAlign` or `width` for positioning
|
||||
|
||||
**Arrow**:
|
||||
```json
|
||||
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0,
|
||||
"points": [[0,0],[200,0]], "endArrowhead": "arrow" }
|
||||
```
|
||||
- `points`: `[dx, dy]` offsets from element `x`, `y`
|
||||
- `endArrowhead`: `null` | `"arrow"` | `"bar"` | `"dot"` | `"triangle"`
|
||||
- `strokeStyle`: `"solid"` (default) | `"dashed"` | `"dotted"`
|
||||
|
||||
### Arrow Bindings (connect arrows to shapes)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 150, "height": 0,
|
||||
"points": [[0,0],[150,0]], "endArrowhead": "arrow",
|
||||
"startBinding": { "elementId": "r1", "fixedPoint": [1, 0.5] },
|
||||
"endBinding": { "elementId": "r2", "fixedPoint": [0, 0.5] }
|
||||
}
|
||||
```
|
||||
|
||||
`fixedPoint` coordinates: `top=[0.5,0]`, `bottom=[0.5,1]`, `left=[0,0.5]`, `right=[1,0.5]`
|
||||
|
||||
### Drawing Order (z-order)
|
||||
- Array order = z-order (first = back, last = front)
|
||||
- Emit progressively: background zones → shape → its bound text → its arrows → next shape
|
||||
- BAD: all rectangles, then all texts, then all arrows
|
||||
- GOOD: bg_zone → shape1 → text_for_shape1 → arrow1 → arrow_label_text → shape2 → text_for_shape2 → ...
|
||||
- Always place the bound text element immediately after its container shape
|
||||
|
||||
### Sizing Guidelines
|
||||
|
||||
**Font sizes:**
|
||||
- Minimum `fontSize`: **16** for body text, labels, descriptions
|
||||
- Minimum `fontSize`: **20** for titles and headings
|
||||
- Minimum `fontSize`: **14** for secondary annotations only (sparingly)
|
||||
- NEVER use `fontSize` below 14
|
||||
|
||||
**Element sizes:**
|
||||
- Minimum shape size: 120x60 for labeled rectangles/ellipses
|
||||
- Leave 20-30px gaps between elements minimum
|
||||
- Prefer fewer, larger elements over many tiny ones
|
||||
|
||||
### Color Palette
|
||||
|
||||
See `references/colors.md` for full color tables. Quick reference:
|
||||
|
||||
| Use | Fill Color | Hex |
|
||||
|-----|-----------|-----|
|
||||
| Primary / Input | Light Blue | `#a5d8ff` |
|
||||
| Success / Output | Light Green | `#b2f2bb` |
|
||||
| Warning / External | Light Orange | `#ffd8a8` |
|
||||
| Processing / Special | Light Purple | `#d0bfff` |
|
||||
| Error / Critical | Light Red | `#ffc9c9` |
|
||||
| Notes / Decisions | Light Yellow | `#fff3bf` |
|
||||
| Storage / Data | Light Teal | `#c3fae8` |
|
||||
|
||||
### Tips
|
||||
- Use the color palette consistently across the diagram
|
||||
- **Text contrast is CRITICAL** -- never use light gray on white backgrounds. Minimum text color on white: `#757575`
|
||||
- Do NOT use emoji in text -- they don't render in Excalidraw's font
|
||||
- For dark mode diagrams, see `references/dark-mode.md`
|
||||
- For larger examples, see `references/examples.md`
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Excalidraw Color Palette
|
||||
|
||||
Use these colors consistently across diagrams.
|
||||
|
||||
## Primary Colors (for strokes, arrows, and accents)
|
||||
|
||||
| Name | Hex | Use |
|
||||
|------|-----|-----|
|
||||
| Blue | `#4a9eed` | Primary actions, links, data series 1 |
|
||||
| Amber | `#f59e0b` | Warnings, highlights, data series 2 |
|
||||
| Green | `#22c55e` | Success, positive, data series 3 |
|
||||
| Red | `#ef4444` | Errors, negative, data series 4 |
|
||||
| Purple | `#8b5cf6` | Accents, special items, data series 5 |
|
||||
| Pink | `#ec4899` | Decorative, data series 6 |
|
||||
| Cyan | `#06b6d4` | Info, secondary, data series 7 |
|
||||
| Lime | `#84cc16` | Extra, data series 8 |
|
||||
|
||||
## Pastel Fills (for shape backgrounds)
|
||||
|
||||
| Color | Hex | Good For |
|
||||
|-------|-----|----------|
|
||||
| Light Blue | `#a5d8ff` | Input, sources, primary nodes |
|
||||
| Light Green | `#b2f2bb` | Success, output, completed |
|
||||
| Light Orange | `#ffd8a8` | Warning, pending, external |
|
||||
| Light Purple | `#d0bfff` | Processing, middleware, special |
|
||||
| Light Red | `#ffc9c9` | Error, critical, alerts |
|
||||
| Light Yellow | `#fff3bf` | Notes, decisions, planning |
|
||||
| Light Teal | `#c3fae8` | Storage, data, memory |
|
||||
| Light Pink | `#eebefa` | Analytics, metrics |
|
||||
|
||||
## Background Zones (use with opacity: 30-35 for layered diagrams)
|
||||
|
||||
| Color | Hex | Good For |
|
||||
|-------|-----|----------|
|
||||
| Blue zone | `#dbe4ff` | UI / frontend layer |
|
||||
| Purple zone | `#e5dbff` | Logic / agent layer |
|
||||
| Green zone | `#d3f9d8` | Data / tool layer |
|
||||
|
||||
## Text Contrast Rules
|
||||
|
||||
- **On white backgrounds**: minimum text color is `#757575`. Default `#1e1e1e` is best.
|
||||
- **Colored text on light fills**: use dark variants (`#15803d` not `#22c55e`, `#2563eb` not `#4a9eed`)
|
||||
- **White text**: only on dark backgrounds (`#9a5030` not `#c4795b`)
|
||||
- **Never**: light gray (`#b0b0b0`, `#999`) on white -- unreadable
|
||||
@@ -0,0 +1,68 @@
|
||||
# Excalidraw Dark Mode Diagrams
|
||||
|
||||
To create a dark-themed diagram, use a massive dark background rectangle as the **first element** in the array. Make it large enough to cover any viewport:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "rectangle", "id": "darkbg",
|
||||
"x": -4000, "y": -3000, "width": 10000, "height": 7500,
|
||||
"backgroundColor": "#1e1e2e", "fillStyle": "solid",
|
||||
"strokeColor": "transparent", "strokeWidth": 0
|
||||
}
|
||||
```
|
||||
|
||||
Then use the following color palettes for elements on the dark background.
|
||||
|
||||
## Text Colors (on dark)
|
||||
|
||||
| Color | Hex | Use |
|
||||
|-------|-----|-----|
|
||||
| White | `#e5e5e5` | Primary text, titles |
|
||||
| Muted | `#a0a0a0` | Secondary text, annotations |
|
||||
| NEVER | `#555` or darker | Invisible on dark bg! |
|
||||
|
||||
## Shape Fills (on dark)
|
||||
|
||||
| Color | Hex | Good For |
|
||||
|-------|-----|----------|
|
||||
| Dark Blue | `#1e3a5f` | Primary nodes |
|
||||
| Dark Green | `#1a4d2e` | Success, output |
|
||||
| Dark Purple | `#2d1b69` | Processing, special |
|
||||
| Dark Orange | `#5c3d1a` | Warning, pending |
|
||||
| Dark Red | `#5c1a1a` | Error, critical |
|
||||
| Dark Teal | `#1a4d4d` | Storage, data |
|
||||
|
||||
## Stroke and Arrow Colors (on dark)
|
||||
|
||||
Use the standard Primary Colors from the main color palette -- they're bright enough on dark backgrounds:
|
||||
- Blue `#4a9eed`, Amber `#f59e0b`, Green `#22c55e`, Red `#ef4444`, Purple `#8b5cf6`
|
||||
|
||||
For subtle shape borders, use `#555555`.
|
||||
|
||||
## Example: Dark mode labeled rectangle
|
||||
|
||||
Use container binding (NOT the `"label"` property, which doesn't work). On dark backgrounds, set text `strokeColor` to `"#e5e5e5"` so it's visible:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "rectangle", "id": "r1",
|
||||
"x": 100, "y": 100, "width": 200, "height": 80,
|
||||
"backgroundColor": "#1e3a5f", "fillStyle": "solid",
|
||||
"strokeColor": "#4a9eed", "strokeWidth": 2,
|
||||
"roundness": { "type": 3 },
|
||||
"boundElements": [{ "id": "t_r1", "type": "text" }]
|
||||
},
|
||||
{
|
||||
"type": "text", "id": "t_r1",
|
||||
"x": 105, "y": 120, "width": 190, "height": 25,
|
||||
"text": "Dark Node", "fontSize": 20, "fontFamily": 1,
|
||||
"strokeColor": "#e5e5e5",
|
||||
"textAlign": "center", "verticalAlign": "middle",
|
||||
"containerId": "r1", "originalText": "Dark Node", "autoResize": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Note: For standalone text elements on dark backgrounds, always set `"strokeColor": "#e5e5e5"` explicitly. The default `#1e1e1e` is invisible on dark.
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# Excalidraw Diagram Examples
|
||||
|
||||
Complete, copy-pasteable examples. Wrap each in the `.excalidraw` envelope before saving:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "hermes-agent",
|
||||
"elements": [ ...elements from examples below... ],
|
||||
"appState": { "viewBackgroundColor": "#ffffff" }
|
||||
}
|
||||
```
|
||||
|
||||
> **IMPORTANT:** All text labels on shapes and arrows use container binding (`containerId` + `boundElements`).
|
||||
> Do NOT use the non-existent `"label"` property -- it will be silently ignored, producing blank shapes.
|
||||
|
||||
---
|
||||
|
||||
## Example 1: Two Connected Labeled Boxes
|
||||
|
||||
A minimal flowchart with two boxes and an arrow between them.
|
||||
|
||||
```json
|
||||
[
|
||||
{ "type": "text", "id": "title", "x": 280, "y": 30, "text": "Simple Flow", "fontSize": 28, "fontFamily": 1, "strokeColor": "#1e1e1e", "originalText": "Simple Flow", "autoResize": true },
|
||||
{ "type": "rectangle", "id": "b1", "x": 100, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid", "boundElements": [{ "id": "t_b1", "type": "text" }, { "id": "a1", "type": "arrow" }] },
|
||||
{ "type": "text", "id": "t_b1", "x": 105, "y": 130, "width": 190, "height": 25, "text": "Start", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e", "textAlign": "center", "verticalAlign": "middle", "containerId": "b1", "originalText": "Start", "autoResize": true },
|
||||
{ "type": "rectangle", "id": "b2", "x": 450, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid", "boundElements": [{ "id": "t_b2", "type": "text" }, { "id": "a1", "type": "arrow" }] },
|
||||
{ "type": "text", "id": "t_b2", "x": 455, "y": 130, "width": 190, "height": 25, "text": "End", "fontSize": 20, "fontFamily": 1, "strokeColor": "#1e1e1e", "textAlign": "center", "verticalAlign": "middle", "containerId": "b2", "originalText": "End", "autoResize": true },
|
||||
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 150, "height": 0, "points": [[0,0],[150,0]], "endArrowhead": "arrow", "startBinding": { "elementId": "b1", "fixedPoint": [1, 0.5] }, "endBinding": { "elementId": "b2", "fixedPoint": [0, 0.5] } }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Photosynthesis Process Diagram
|
||||
|
||||
A larger diagram with background zones, multiple nodes, and directional arrows showing inputs/outputs.
|
||||
|
||||
```json
|
||||
[
|
||||
{"type":"text","id":"ti","x":280,"y":10,"text":"Photosynthesis","fontSize":28,"fontFamily":1,"strokeColor":"#1e1e1e","originalText":"Photosynthesis","autoResize":true},
|
||||
{"type":"text","id":"fo","x":245,"y":48,"text":"6CO2 + 6H2O --> C6H12O6 + 6O2","fontSize":16,"fontFamily":1,"strokeColor":"#757575","originalText":"6CO2 + 6H2O --> C6H12O6 + 6O2","autoResize":true},
|
||||
{"type":"rectangle","id":"lf","x":150,"y":90,"width":520,"height":380,"backgroundColor":"#d3f9d8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","strokeWidth":1,"opacity":35},
|
||||
{"type":"text","id":"lfl","x":170,"y":96,"text":"Inside the Leaf","fontSize":16,"fontFamily":1,"strokeColor":"#15803d","originalText":"Inside the Leaf","autoResize":true},
|
||||
|
||||
{"type":"rectangle","id":"lr","x":190,"y":190,"width":160,"height":70,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","boundElements":[{"id":"t_lr","type":"text"},{"id":"a1","type":"arrow"},{"id":"a2","type":"arrow"},{"id":"a3","type":"arrow"},{"id":"a5","type":"arrow"}]},
|
||||
{"type":"text","id":"t_lr","x":195,"y":205,"width":150,"height":20,"text":"Light Reactions","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"lr","originalText":"Light Reactions","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"a1","x":350,"y":225,"width":120,"height":0,"points":[[0,0],[120,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","boundElements":[{"id":"t_a1","type":"text"}]},
|
||||
{"type":"text","id":"t_a1","x":390,"y":205,"width":40,"height":20,"text":"ATP","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"a1","originalText":"ATP","autoResize":true},
|
||||
|
||||
{"type":"rectangle","id":"cc","x":470,"y":190,"width":160,"height":70,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#8b5cf6","boundElements":[{"id":"t_cc","type":"text"},{"id":"a1","type":"arrow"},{"id":"a4","type":"arrow"},{"id":"a6","type":"arrow"}]},
|
||||
{"type":"text","id":"t_cc","x":475,"y":205,"width":150,"height":20,"text":"Calvin Cycle","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"cc","originalText":"Calvin Cycle","autoResize":true},
|
||||
|
||||
{"type":"rectangle","id":"sl","x":10,"y":200,"width":120,"height":50,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","boundElements":[{"id":"t_sl","type":"text"},{"id":"a2","type":"arrow"}]},
|
||||
{"type":"text","id":"t_sl","x":15,"y":210,"width":110,"height":20,"text":"Sunlight","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"sl","originalText":"Sunlight","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"a2","x":130,"y":225,"width":60,"height":0,"points":[[0,0],[60,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow"},
|
||||
|
||||
{"type":"rectangle","id":"wa","x":200,"y":360,"width":140,"height":50,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","boundElements":[{"id":"t_wa","type":"text"},{"id":"a3","type":"arrow"}]},
|
||||
{"type":"text","id":"t_wa","x":205,"y":370,"width":130,"height":20,"text":"Water (H2O)","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"wa","originalText":"Water (H2O)","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"a3","x":270,"y":360,"width":0,"height":-100,"points":[[0,0],[0,-100]],"strokeColor":"#4a9eed","strokeWidth":2,"endArrowhead":"arrow"},
|
||||
|
||||
{"type":"rectangle","id":"co","x":480,"y":360,"width":130,"height":50,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","boundElements":[{"id":"t_co","type":"text"},{"id":"a4","type":"arrow"}]},
|
||||
{"type":"text","id":"t_co","x":485,"y":370,"width":120,"height":20,"text":"CO2","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"co","originalText":"CO2","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"a4","x":545,"y":360,"width":0,"height":-100,"points":[[0,0],[0,-100]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow"},
|
||||
|
||||
{"type":"rectangle","id":"ox","x":540,"y":100,"width":100,"height":40,"backgroundColor":"#ffc9c9","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#ef4444","boundElements":[{"id":"t_ox","type":"text"},{"id":"a5","type":"arrow"}]},
|
||||
{"type":"text","id":"t_ox","x":545,"y":105,"width":90,"height":20,"text":"O2","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"ox","originalText":"O2","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"a5","x":310,"y":190,"width":230,"height":-50,"points":[[0,0],[230,-50]],"strokeColor":"#ef4444","strokeWidth":2,"endArrowhead":"arrow"},
|
||||
|
||||
{"type":"rectangle","id":"gl","x":690,"y":195,"width":120,"height":60,"backgroundColor":"#c3fae8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","boundElements":[{"id":"t_gl","type":"text"},{"id":"a6","type":"arrow"}]},
|
||||
{"type":"text","id":"t_gl","x":695,"y":210,"width":110,"height":25,"text":"Glucose","fontSize":18,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"gl","originalText":"Glucose","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"a6","x":630,"y":225,"width":60,"height":0,"points":[[0,0],[60,0]],"strokeColor":"#22c55e","strokeWidth":2,"endArrowhead":"arrow"},
|
||||
|
||||
{"type":"ellipse","id":"sun","x":30,"y":110,"width":50,"height":50,"backgroundColor":"#fff3bf","fillStyle":"solid","strokeColor":"#f59e0b","strokeWidth":2},
|
||||
{"type":"arrow","id":"r1","x":55,"y":108,"width":0,"height":-14,"points":[[0,0],[0,-14]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
|
||||
{"type":"arrow","id":"r2","x":55,"y":162,"width":0,"height":14,"points":[[0,0],[0,14]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
|
||||
{"type":"arrow","id":"r3","x":28,"y":135,"width":-14,"height":0,"points":[[0,0],[-14,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
|
||||
{"type":"arrow","id":"r4","x":82,"y":135,"width":14,"height":0,"points":[[0,0],[14,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Sequence Diagram (UML-style)
|
||||
|
||||
Demonstrates a sequence diagram with actors, dashed lifelines, and message arrows.
|
||||
|
||||
```json
|
||||
[
|
||||
{"type":"text","id":"title","x":200,"y":15,"text":"MCP Apps -- Sequence Flow","fontSize":24,"fontFamily":1,"strokeColor":"#1e1e1e","originalText":"MCP Apps -- Sequence Flow","autoResize":true},
|
||||
|
||||
{"type":"rectangle","id":"uHead","x":60,"y":60,"width":100,"height":40,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2,"boundElements":[{"id":"t_uHead","type":"text"}]},
|
||||
{"type":"text","id":"t_uHead","x":65,"y":65,"width":90,"height":20,"text":"User","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"uHead","originalText":"User","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"uLine","x":110,"y":100,"width":0,"height":400,"points":[[0,0],[0,400]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
|
||||
|
||||
{"type":"rectangle","id":"aHead","x":230,"y":60,"width":100,"height":40,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#8b5cf6","strokeWidth":2,"boundElements":[{"id":"t_aHead","type":"text"}]},
|
||||
{"type":"text","id":"t_aHead","x":235,"y":65,"width":90,"height":20,"text":"Agent","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"aHead","originalText":"Agent","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"aLine","x":280,"y":100,"width":0,"height":400,"points":[[0,0],[0,400]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
|
||||
|
||||
{"type":"rectangle","id":"sHead","x":420,"y":60,"width":130,"height":40,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":2,"boundElements":[{"id":"t_sHead","type":"text"}]},
|
||||
{"type":"text","id":"t_sHead","x":425,"y":65,"width":120,"height":20,"text":"Server","fontSize":16,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"sHead","originalText":"Server","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"sLine","x":485,"y":100,"width":0,"height":400,"points":[[0,0],[0,400]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
|
||||
|
||||
{"type":"arrow","id":"m1","x":110,"y":150,"width":170,"height":0,"points":[[0,0],[170,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","boundElements":[{"id":"t_m1","type":"text"}]},
|
||||
{"type":"text","id":"t_m1","x":165,"y":130,"width":60,"height":20,"text":"request","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m1","originalText":"request","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"m2","x":280,"y":200,"width":205,"height":0,"points":[[0,0],[205,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","boundElements":[{"id":"t_m2","type":"text"}]},
|
||||
{"type":"text","id":"t_m2","x":352,"y":180,"width":60,"height":20,"text":"tools/call","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m2","originalText":"tools/call","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"m3","x":485,"y":260,"width":-205,"height":0,"points":[[0,0],[-205,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","boundElements":[{"id":"t_m3","type":"text"}]},
|
||||
{"type":"text","id":"t_m3","x":352,"y":240,"width":60,"height":20,"text":"result","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m3","originalText":"result","autoResize":true},
|
||||
|
||||
{"type":"arrow","id":"m4","x":280,"y":320,"width":-170,"height":0,"points":[[0,0],[-170,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","boundElements":[{"id":"t_m4","type":"text"}]},
|
||||
{"type":"text","id":"t_m4","x":165,"y":300,"width":60,"height":20,"text":"response","fontSize":14,"fontFamily":1,"strokeColor":"#1e1e1e","textAlign":"center","verticalAlign":"middle","containerId":"m4","originalText":"response","autoResize":true}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
- **Do NOT use `"label"` property** -- this is the #1 mistake. It is NOT part of the Excalidraw file format and will be silently ignored, producing blank shapes with no visible text. Always use container binding (`containerId` + `boundElements`) as shown in the examples above.
|
||||
- **Every bound text needs both sides linked** -- the shape needs `boundElements: [{"id": "t_xxx", "type": "text"}]` AND the text needs `containerId: "shape_id"`. If either is missing, the binding won't work.
|
||||
- **Include `originalText` and `autoResize: true`** on all text elements -- Excalidraw uses these for proper text reflow.
|
||||
- **Include `fontFamily: 1`** on all text elements -- without it, text may not render with the expected hand-drawn font.
|
||||
- **Elements overlap when y-coordinates are close** -- always check that text, boxes, and labels don't stack on top of each other
|
||||
- **Arrow labels need space** -- long labels like "ATP + NADPH" overflow short arrows. Keep labels short or make arrows wider
|
||||
- **Center titles relative to the diagram** -- estimate total width and center the title text over it
|
||||
- **Draw decorations LAST** -- cute illustrations (sun, stars, icons) should appear at the end of the array so they're drawn on top
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Upload an .excalidraw file to excalidraw.com and print a shareable URL.
|
||||
|
||||
No account required. The diagram is encrypted client-side (AES-GCM) before
|
||||
upload -- the encryption key is embedded in the URL fragment, so the server
|
||||
never sees plaintext.
|
||||
|
||||
Requirements:
|
||||
pip install cryptography
|
||||
|
||||
Usage:
|
||||
python upload.py <path-to-file.excalidraw>
|
||||
|
||||
Example:
|
||||
python upload.py ~/diagrams/architecture.excalidraw
|
||||
# prints: https://excalidraw.com/#json=abc123,encryptionKeyHere
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
import base64
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
except ImportError:
|
||||
print("Error: 'cryptography' package is required for upload.")
|
||||
print("Install it with: pip install cryptography")
|
||||
sys.exit(1)
|
||||
|
||||
# Excalidraw public upload endpoint (no auth needed)
|
||||
UPLOAD_URL = "https://json.excalidraw.com/api/v2/post/"
|
||||
|
||||
|
||||
def concat_buffers(*buffers: bytes) -> bytes:
|
||||
"""
|
||||
Build the Excalidraw v2 concat-buffers binary format.
|
||||
|
||||
Layout: [version=1 (4B big-endian)] then for each buffer:
|
||||
[length (4B big-endian)] [data bytes]
|
||||
"""
|
||||
parts = [struct.pack(">I", 1)] # version = 1
|
||||
for buf in buffers:
|
||||
parts.append(struct.pack(">I", len(buf)))
|
||||
parts.append(buf)
|
||||
return b"".join(parts)
|
||||
|
||||
|
||||
def upload(excalidraw_json: str) -> str:
|
||||
"""
|
||||
Encrypt and upload Excalidraw JSON to excalidraw.com.
|
||||
|
||||
Args:
|
||||
excalidraw_json: The full .excalidraw file content as a string.
|
||||
|
||||
Returns:
|
||||
Shareable URL string.
|
||||
"""
|
||||
# 1. Inner payload: concat_buffers(file_metadata, data)
|
||||
file_metadata = json.dumps({}).encode("utf-8")
|
||||
data_bytes = excalidraw_json.encode("utf-8")
|
||||
inner_payload = concat_buffers(file_metadata, data_bytes)
|
||||
|
||||
# 2. Compress with zlib
|
||||
compressed = zlib.compress(inner_payload)
|
||||
|
||||
# 3. AES-GCM 128-bit encrypt
|
||||
raw_key = os.urandom(16) # 128-bit key
|
||||
iv = os.urandom(12) # 12-byte nonce
|
||||
aesgcm = AESGCM(raw_key)
|
||||
encrypted = aesgcm.encrypt(iv, compressed, None)
|
||||
|
||||
# 4. Encoding metadata
|
||||
encoding_meta = json.dumps({
|
||||
"version": 2,
|
||||
"compression": "pako@1",
|
||||
"encryption": "AES-GCM",
|
||||
}).encode("utf-8")
|
||||
|
||||
# 5. Outer payload: concat_buffers(encoding_meta, iv, encrypted)
|
||||
payload = concat_buffers(encoding_meta, iv, encrypted)
|
||||
|
||||
# 6. Upload
|
||||
req = urllib.request.Request(UPLOAD_URL, data=payload, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"Upload failed with HTTP {resp.status}")
|
||||
result = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
file_id = result.get("id")
|
||||
if not file_id:
|
||||
raise RuntimeError(f"Upload returned no file ID. Response: {result}")
|
||||
|
||||
# 7. Key as base64url (JWK 'k' format, no padding)
|
||||
key_b64 = base64.urlsafe_b64encode(raw_key).rstrip(b"=").decode("ascii")
|
||||
|
||||
return f"https://excalidraw.com/#json={file_id},{key_b64}"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python upload.py <path-to-file.excalidraw>")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"Error: File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Basic validation: should be valid JSON with an "elements" key
|
||||
try:
|
||||
doc = json.loads(content)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: File is not valid JSON: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if "elements" not in doc:
|
||||
print("Warning: File does not contain an 'elements' key. Uploading anyway.")
|
||||
|
||||
url = upload(content)
|
||||
print(url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user