@restless-dreams/orbit 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +165 -2
- package/dist/bin/orbit.js +8907 -0
- package/dist/bin/orbit.js.map +48 -0
- package/dist/chunk-0gwh723s.js +408 -0
- package/dist/chunk-0gwh723s.js.map +10 -0
- package/dist/chunk-7n00wx0t.js +82 -0
- package/dist/chunk-7n00wx0t.js.map +10 -0
- package/dist/chunk-mtrnm0yc.js +16993 -0
- package/dist/chunk-mtrnm0yc.js.map +63 -0
- package/dist/core/auto-render.d.ts +85 -0
- package/dist/core/browser.d.ts +23 -0
- package/dist/core/create.d.ts +23 -0
- package/dist/core/diagnostics.d.ts +40 -0
- package/dist/core/dispatch.d.ts +23 -0
- package/dist/core/document-options.d.ts +18 -0
- package/dist/core/document-source.d.ts +128 -0
- package/dist/core/editing.d.ts +63 -0
- package/dist/core/engine/animation-clock.d.ts +40 -0
- package/dist/core/engine/camera.d.ts +70 -0
- package/dist/core/engine/colors.d.ts +51 -0
- package/dist/core/engine/display.d.ts +125 -0
- package/dist/core/engine/export.d.ts +32 -0
- package/dist/core/engine/glide.d.ts +34 -0
- package/dist/core/engine/labels.d.ts +106 -0
- package/dist/core/engine/node-drag.d.ts +49 -0
- package/dist/core/engine/scene.d.ts +159 -0
- package/dist/core/engine/spin.d.ts +62 -0
- package/dist/core/engine/surface-pattern.d.ts +20 -0
- package/dist/core/engine/text-measure.d.ts +116 -0
- package/dist/core/engine/views.d.ts +353 -0
- package/dist/core/extensions.d.ts +155 -0
- package/dist/core/format-upgrades.d.ts +33 -0
- package/dist/core/index.d.ts +36 -0
- package/dist/core/input-controller.d.ts +109 -0
- package/dist/core/languages.d.ts +199 -0
- package/dist/core/layout.d.ts +61 -0
- package/dist/core/map-tools/definitions.d.ts +71 -0
- package/dist/core/map-tools/input.d.ts +62 -0
- package/dist/core/map-tools/reading.d.ts +40 -0
- package/dist/core/map-tools/tools.d.ts +51 -0
- package/dist/core/markdown-writing.d.ts +87 -0
- package/dist/core/material.d.ts +25 -0
- package/dist/core/media.d.ts +42 -0
- package/dist/core/node-attributes.d.ts +31 -0
- package/dist/core/node-links.d.ts +25 -0
- package/dist/core/parser-markdown.d.ts +42 -0
- package/dist/core/parser-yaml.d.ts +8 -0
- package/dist/core/render-to-svg.d.ts +57 -0
- package/dist/core/serializer-markdown.d.ts +18 -0
- package/dist/core/serializer-yaml.d.ts +2 -0
- package/dist/core/source-patch.d.ts +24 -0
- package/dist/core/source.d.ts +27 -0
- package/dist/core/styles.d.ts +6 -0
- package/dist/core/suggest.d.ts +8 -0
- package/dist/core/svg-renderer.d.ts +729 -0
- package/dist/core/text-layout.d.ts +54 -0
- package/dist/core/themes.d.ts +62 -0
- package/dist/core/types.d.ts +463 -0
- package/dist/core/ui/actions.d.ts +75 -0
- package/dist/core/ui/controls.d.ts +61 -0
- package/dist/core/ui/detail-sheet.d.ts +88 -0
- package/dist/core/ui/frames.d.ts +11 -0
- package/dist/core/ui/icons.d.ts +14 -0
- package/dist/core/ui/inline-editor.d.ts +79 -0
- package/dist/core/ui/media-players.d.ts +65 -0
- package/dist/core/ui/media-viewer.d.ts +169 -0
- package/dist/core/ui/media-zoom.d.ts +51 -0
- package/dist/core/ui/note-markdown.d.ts +19 -0
- package/dist/core/ui/outline.d.ts +44 -0
- package/dist/core/url-guard.d.ts +40 -0
- package/dist/core/utils.d.ts +23 -0
- package/dist/core/web-page/page.d.ts +58 -0
- package/dist/core/web-page/runtime.d.ts +13 -0
- package/dist/core/web-page/start.d.ts +1 -0
- package/dist/core/yaml-mini.d.ts +54 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1448 -0
- package/dist/index.js.map +17 -0
- package/dist/marked/index.d.ts +31 -0
- package/dist/marked/index.js +110 -0
- package/dist/marked/index.js.map +10 -0
- package/dist/react/index.d.ts +55 -0
- package/dist/react/index.js +352 -0
- package/dist/react/index.js.map +1 -0
- package/dist/web-page.d.ts +2 -0
- package/dist/web-page.js +130 -0
- package/llms-full.txt +2585 -0
- package/llms.txt +35 -0
- package/package.json +118 -4
- package/skills/orbit/SKILL.md +140 -0
- package/skills/orbit/references/editing.md +194 -0
- package/skills/orbit/references/extending.md +247 -0
- package/skills/orbit/references/format.md +303 -0
- package/skills/orbit/references/guide.md +522 -0
- package/skills/orbit/references/languages.md +159 -0
- package/skills/orbit/references/map-tools.md +188 -0
- package/skills/orbit/references/media.md +160 -0
- package/skills/orbit/references/recipes.md +227 -0
- package/skills/orbit/references/theming.md +184 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<!-- A copy of docs/languages.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
|
|
2
|
+
|
|
3
|
+
# Languages
|
|
4
|
+
|
|
5
|
+
Orbit speaks the language of the page it's on: its buttons, the detail sheet, the media viewer, the list of the map, its messages and everything screen readers hear about the map. English and Spanish are built in. You can add any other language, change single words, or pick a language for one map.
|
|
6
|
+
|
|
7
|
+
Only Orbit's own words change. The text of a map is the author's, and so are the status words written in it (`status: done` shows as "done"). Problems Orbit reports about a map's source (in the console, `orbit validate` and diagnostics) stay in English: they're for the map's author and their tools.
|
|
8
|
+
|
|
9
|
+
## Which language a map speaks
|
|
10
|
+
|
|
11
|
+
A map speaks the language of the element it's drawn in: the closest `lang` attribute around it, through shadow roots, and then the page's own `<html lang>`. A page in Spanish gets its maps in Spanish with nothing to set:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<html lang="es">
|
|
15
|
+
…
|
|
16
|
+
<orbit-map># Viaje
|
|
17
|
+
## Comida
|
|
18
|
+
## Lugares</orbit-map>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The `language` option picks a language for one map, whatever the page says:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
createOrbitMap(element, markdown, { language: 'es' });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<OrbitMindMap source={markdown} language="es" />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
On `<orbit-map>`, set its `lang` attribute, as on any element: `<orbit-map lang="es">`.
|
|
32
|
+
|
|
33
|
+
Languages are [BCP 47 tags](https://www.rfc-editor.org/info/bcp47). A regional tag speaks its language with the region's numbers and plurals: `es-MX` and `es-ES` both speak Spanish, writing 24,000 and 24.000. A language Orbit doesn't have speaks English, and so does a map on a page that names none.
|
|
34
|
+
|
|
35
|
+
Changing the language later, with `setOptions({ language })`, the `language` prop or the element's `lang` attribute, writes the map's words again where they are.
|
|
36
|
+
|
|
37
|
+
## Adding a language
|
|
38
|
+
|
|
39
|
+
`registerLanguage` adds a language for every map, by its tag. Words it leaves out are English, or, for a regional tag like `pt-BR`, its base language's (`pt`) when Orbit has it:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { registerLanguage } from '@restless-dreams/orbit';
|
|
43
|
+
|
|
44
|
+
registerLanguage('fr', {
|
|
45
|
+
focus: 'Focus',
|
|
46
|
+
overview: 'Vue d’ensemble',
|
|
47
|
+
zoomIn: 'Zoomer',
|
|
48
|
+
zoomOut: 'Dézoomer',
|
|
49
|
+
ideaCount: { one: '{count} idée', many: '{count} d’idées', other: '{count} idées' },
|
|
50
|
+
// …the rest of the words in the table below.
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It returns a function that takes the language away again. Registering a language Orbit already has changes its words for every map: `registerLanguage('es', { video: 'Vídeo' })`. `languageTags()` lists the languages maps can speak.
|
|
55
|
+
|
|
56
|
+
### Placeholders, plurals and lists
|
|
57
|
+
|
|
58
|
+
- **Placeholders:** words in braces (`{name}`, `{count}`) are filled in for you. Keep them in a translation, in whatever order the language needs.
|
|
59
|
+
- **Plurals:** text with a number is an object of plural forms, one for each [plural category](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/select) the language uses (`zero`, `one`, `two`, `few`, `many`) and `other`, which is required and covers any form left out.
|
|
60
|
+
- **Numbers and lists:** numbers are written the way the language writes them, and lists like "has note, image, and link" are joined the language's way ("tiene nota, imagen y enlace").
|
|
61
|
+
|
|
62
|
+
## Changing single words
|
|
63
|
+
|
|
64
|
+
The `strings` option changes words on one map, over its language's:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
createOrbitMap(element, markdown, { strings: { zoomIn: 'Closer', zoomOut: 'Further' } });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A word Orbit doesn't have, or one of the wrong kind, is skipped with a warning in the console.
|
|
71
|
+
|
|
72
|
+
## The root Orbit makes up
|
|
73
|
+
|
|
74
|
+
A map that's just a list, or has several top headings, and has no title gets a root Orbit makes up, named "Untitled map" in the map's language. When you parse maps yourself, `parseOrbit(source, { untitled: 'Mapa sin título' })` gives it that name. Its id is `untitled-map` in every language, so links and saved places keep working, and saving the map writes it as the list it was.
|
|
75
|
+
|
|
76
|
+
## Without a browser
|
|
77
|
+
|
|
78
|
+
`renderToSVGString(markdown, { language: 'es' })` writes the drawing's few words in a language: an untitled idea's name, the empty map's message and the map's title when it has none. On the command line: `orbit render map.md --language es`.
|
|
79
|
+
|
|
80
|
+
## Every word
|
|
81
|
+
|
|
82
|
+
| Key | English | Spanish |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `mindMap` | Mind map | Mapa mental |
|
|
85
|
+
| `mindMapRole` | mind map | mapa mental |
|
|
86
|
+
| `untitled` | Untitled | Sin título |
|
|
87
|
+
| `untitledMap` | Untitled map | Mapa sin título |
|
|
88
|
+
| `emptyMap` | This map is empty | Este mapa está vacío |
|
|
89
|
+
| `emptyMapHint` | Start it with a # heading or a bullet list | Empiézalo con un encabezado # o una lista con viñetas |
|
|
90
|
+
| `mapNotDrawn` | This map couldn’t be drawn | No se pudo dibujar este mapa |
|
|
91
|
+
| `mapNotDrawnReason` | The map could not be drawn. | No se pudo dibujar el mapa. |
|
|
92
|
+
| `problemOnLine` | Line {line}: {message} | Línea {line}: {message} |
|
|
93
|
+
| `mapPath` | Map path | Ruta del mapa |
|
|
94
|
+
| `back` | Back | Atrás |
|
|
95
|
+
| `backTo` | Back to {name} | Volver a {name} |
|
|
96
|
+
| `backToParent` | Back to the parent node | Volver a la idea superior |
|
|
97
|
+
| `view` | View | Vista |
|
|
98
|
+
| `focus` | Focus | Enfoque |
|
|
99
|
+
| `overview` | Overview | Vista general |
|
|
100
|
+
| `showOutline` | Show the map as a list | Mostrar el mapa como lista |
|
|
101
|
+
| `zoom` | Zoom | Zoom |
|
|
102
|
+
| `zoomIn` | Zoom in | Acercar |
|
|
103
|
+
| `zoomOut` | Zoom out | Alejar |
|
|
104
|
+
| `fitMap` | Fit the map | Ajustar el mapa |
|
|
105
|
+
| `scrollToZoom` | Use {key} + scroll to zoom the map | Usa {key} + desplazamiento para acercar o alejar el mapa |
|
|
106
|
+
| `outline` | Map outline | Esquema del mapa |
|
|
107
|
+
| `overviewAnnouncement` | Overview of the whole map | Vista general de todo el mapa |
|
|
108
|
+
| `ideaCount` | one: {count} item; other: {count} items | one: {count} idea; many: {count} ideas; other: {count} ideas |
|
|
109
|
+
| `added` | Added {name} | Se agregó {name} |
|
|
110
|
+
| `removed` | Removed {name} | Se eliminó {name} |
|
|
111
|
+
| `moved` | Moved {name} to {parent} | Se movió {name} a {parent} |
|
|
112
|
+
| `movedToTop` | Moved {name} to the top | Se movió {name} al nivel superior |
|
|
113
|
+
| `anIdea` | a node | una idea |
|
|
114
|
+
| `status` | status {status} | estado {status} |
|
|
115
|
+
| `statusLabel` | Status: {status} | Estado: {status} |
|
|
116
|
+
| `has` | has {list} | tiene {list} |
|
|
117
|
+
| `hasDetails` | details | detalles |
|
|
118
|
+
| `hasNote` | note | nota |
|
|
119
|
+
| `hasImage` | image | imagen |
|
|
120
|
+
| `hasVideo` | video | video |
|
|
121
|
+
| `hasEmbed` | embed | contenido insertado |
|
|
122
|
+
| `hasLink` | link | enlace |
|
|
123
|
+
| `textOf` | Text of {name} | Texto de {name} |
|
|
124
|
+
| `newIdeaFor` | New idea for {name} | Idea nueva en {name} |
|
|
125
|
+
| `newIdea` | New idea | Idea nueva |
|
|
126
|
+
| `addIdeaTo` | Add an idea to {name} | Agregar una idea a {name} |
|
|
127
|
+
| `moveHere` | Move {name} here | Mover {name} aquí |
|
|
128
|
+
| `thisIdea` | this node | esta idea |
|
|
129
|
+
| `theIdea` | the idea | la idea |
|
|
130
|
+
| `ideaActions` | {name} actions | Acciones de {name} |
|
|
131
|
+
| `ideaActionsUntitled` | Node actions | Acciones de la idea |
|
|
132
|
+
| `showDetails` | Show details | Ver detalles |
|
|
133
|
+
| `viewImage` | View image | Ver imagen |
|
|
134
|
+
| `viewImageOf` | View image: {description} | Ver imagen: {description} |
|
|
135
|
+
| `playVideo` | Play video | Reproducir video |
|
|
136
|
+
| `openEmbed` | Open embed | Abrir contenido insertado |
|
|
137
|
+
| `readNote` | Read note | Leer nota |
|
|
138
|
+
| `openLink` | Open link | Abrir enlace |
|
|
139
|
+
| `goTo` | Go to {name} | Ir a {name} |
|
|
140
|
+
| `details` | Details | Detalles |
|
|
141
|
+
| `close` | Close | Cerrar |
|
|
142
|
+
| `previous` | Previous | Anterior |
|
|
143
|
+
| `next` | Next | Siguiente |
|
|
144
|
+
| `present` | Present | Presentar |
|
|
145
|
+
| `priority` | Priority {priority} | Prioridad {priority} |
|
|
146
|
+
| `expand` | Expand | Ampliar |
|
|
147
|
+
| `expandVideo` | Expand the video | Ampliar el video |
|
|
148
|
+
| `picture` | Picture | Imagen |
|
|
149
|
+
| `video` | Video | Video |
|
|
150
|
+
| `position` | {index} of {total} | {index} de {total} |
|
|
151
|
+
| `fitPicture` | Fit the picture | Ajustar la imagen |
|
|
152
|
+
| `pictureUnavailable` | This picture can’t be shown. | No se puede mostrar esta imagen. |
|
|
153
|
+
| `videoUnavailable` | This video can’t be played. | No se puede reproducir este video. |
|
|
154
|
+
| `videoUnavailableHere` | This video can’t be played here. | Este video no se puede reproducir aquí. |
|
|
155
|
+
| `openPictureInNewTab` | Open the picture in a new tab | Abrir la imagen en una pestaña nueva |
|
|
156
|
+
| `openVideoInNewTab` | Open the video in a new tab | Abrir el video en una pestaña nueva |
|
|
157
|
+
| `watchOn` | Watch on {provider} | Ver en {provider} |
|
|
158
|
+
|
|
159
|
+
`OrbitStrings` in the [API reference](https://github.com/restless-dreams/orbit/blob/main/docs/api/README.md) says where each one shows.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!-- A copy of docs/map-tools.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
|
|
2
|
+
|
|
3
|
+
# Map tools for AI
|
|
4
|
+
|
|
5
|
+
Map tools let a language model read a map and change it: add ideas and whole branches, rename them, mark them done, move, sort and remove them. They come in the shape each provider's API takes, so an app can offer an assistant with whichever model its people choose. They work on a map on screen, where every change animates into place, or on a document on a server. A call a model gets wrong changes nothing, and its result says what to send instead.
|
|
6
|
+
|
|
7
|
+
## Add an assistant to a map
|
|
8
|
+
|
|
9
|
+
`createMapTools(map)` gives the tools for one map. `tools.for(provider)` lists them for the model, and `tools.run(name, input)` carries out each call it makes. With Anthropic's API:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
13
|
+
import { createOrbitMap, createMapTools } from '@restless-dreams/orbit';
|
|
14
|
+
|
|
15
|
+
const map = createOrbitMap(element, markdown, { onEdit: () => save(map.toMarkdown()) });
|
|
16
|
+
const tools = createMapTools(map);
|
|
17
|
+
const client = new Anthropic({ apiKey });
|
|
18
|
+
|
|
19
|
+
const ask = async (request) => {
|
|
20
|
+
const messages = [{ role: 'user', content: request }];
|
|
21
|
+
|
|
22
|
+
while (true) {
|
|
23
|
+
const response = await client.messages.create({ model, max_tokens: 4096, system: tools.instructions, tools: tools.for('anthropic'), messages });
|
|
24
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
25
|
+
const calls = response.content.filter((block) => block.type === 'tool_use');
|
|
26
|
+
|
|
27
|
+
if (calls.length === 0) {
|
|
28
|
+
return response.content.find((block) => block.type === 'text')?.text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
messages.push({
|
|
32
|
+
role: 'user',
|
|
33
|
+
content: calls.map((call) => {
|
|
34
|
+
const result = tools.run(call.name, call.input);
|
|
35
|
+
return { type: 'tool_result', tool_use_id: call.id, content: result.text, is_error: !result.ok };
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
await ask('Add three day trips from Kyoto, each with a one-sentence note');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The map changes as the model works: each new idea flies out of its parent, and `edit` fires for each change, so the map saves as it does when people edit it. Keep API keys off public pages: call the model from your server, or let each person bring their own key.
|
|
45
|
+
|
|
46
|
+
## Other providers
|
|
47
|
+
|
|
48
|
+
**OpenAI's Responses API:**
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const input = [{ role: 'user', content: request }];
|
|
52
|
+
|
|
53
|
+
while (true) {
|
|
54
|
+
const response = await openai.responses.create({ model, instructions: tools.instructions, tools: tools.for('openai'), input });
|
|
55
|
+
const calls = response.output.filter((item) => item.type === 'function_call');
|
|
56
|
+
|
|
57
|
+
if (calls.length === 0) {
|
|
58
|
+
return response.output_text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
input.push(...response.output);
|
|
62
|
+
|
|
63
|
+
for (const call of calls) {
|
|
64
|
+
input.push({ type: 'function_call_output', call_id: call.call_id, output: tools.run(call.name, call.arguments).text });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`run` takes the arguments as OpenAI sends them, as JSON text. The tools are written without strict mode, which would make the model send every field of every call.
|
|
70
|
+
|
|
71
|
+
**Chat Completions,** which most other providers and local servers (OpenRouter, Ollama and many more) offer as well as OpenAI:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const messages = [{ role: 'system', content: tools.instructions }, { role: 'user', content: request }];
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
const response = await client.chat.completions.create({ model, messages, tools: tools.for('openai-chat') });
|
|
78
|
+
const message = response.choices[0].message;
|
|
79
|
+
messages.push(message);
|
|
80
|
+
|
|
81
|
+
if (!message.tool_calls?.length) {
|
|
82
|
+
return message.content;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const call of message.tool_calls) {
|
|
86
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: tools.run(call.function.name, call.function.arguments).text });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Gemini,** with `@google/genai`:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const contents = [{ role: 'user', parts: [{ text: request }] }];
|
|
95
|
+
|
|
96
|
+
while (true) {
|
|
97
|
+
const response = await ai.models.generateContent({ model, contents, config: { systemInstruction: tools.instructions, tools: tools.for('gemini') } });
|
|
98
|
+
const calls = response.functionCalls ?? [];
|
|
99
|
+
|
|
100
|
+
if (calls.length === 0) {
|
|
101
|
+
return response.text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
contents.push(response.candidates[0].content);
|
|
105
|
+
contents.push({
|
|
106
|
+
role: 'user',
|
|
107
|
+
parts: calls.map((call) => {
|
|
108
|
+
const result = tools.run(call.name, call.args);
|
|
109
|
+
return { functionResponse: { name: call.name, response: result.ok ? { output: result.text } : { error: result.text } } };
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**MCP:** `tools.definitions` are tools as MCP lists them (`name`, `description`, `inputSchema` and `annotations`). A server returns them for `tools/list`, and answers `tools/call` with `{ content: [{ type: 'text', text: result.text }], isError: !result.ok }`.
|
|
116
|
+
|
|
117
|
+
For anything else, such as the Vercel AI SDK or LangChain, build the tools from `tools.definitions`: each has a name, a description, and its input as JSON Schema.
|
|
118
|
+
|
|
119
|
+
## The tools
|
|
120
|
+
|
|
121
|
+
| Tool | What it does | Takes |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `read_map` | The map as an outline: each node's title, id and attributes, and the start of its note. A big map is cut short, whole levels first, with what's left out counted. | `node_id` (to read one branch), `depth` |
|
|
124
|
+
| `find_nodes` | Searches titles, notes and tags, in any case and with or without accents, and statuses by what they mean: `done` finds `shipped` too. | `query`, `status`, `tag` |
|
|
125
|
+
| `read_node` | Everything about one node: its whole note, its picture, video or page, where it is, its children, and the nodes that link to it. | `node_id` |
|
|
126
|
+
| `add_nodes` | Adds nodes, or whole branches, written as a markdown list. | `parent_id`, `outline`, `index` |
|
|
127
|
+
| `update_node` | Changes a title or attributes, and `clear` removes attributes. | `node_id`, `text`, `note`, `status`, `priority`, `tags`, `icon`, `color`, `link`, `image`, `video`, `embed`, `collapsed`, `clear` |
|
|
128
|
+
| `move_node` | Moves a branch under another node, or to another place among its siblings. | `node_id`, `parent_id`, `index` |
|
|
129
|
+
| `reorder_children` | Puts all the children of a node in a new order at once: to sort them, or to group them. | `parent_id`, `order` |
|
|
130
|
+
| `remove_node` | Removes a branch. Never the root. | `node_id` |
|
|
131
|
+
| `show_node` | Flies the map to a node. Only on a map on screen. | `node_id` |
|
|
132
|
+
|
|
133
|
+
New ideas come as orbit-md, the way models write any outline: each list item is a node, the items indented under it are its children, at any depth, and indented text that isn't an item is its note. Checkboxes, an emoji first, hashtags at the end and `<!-- orbit … -->` comments set statuses, icons, tags and other attributes, as in any map:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"parent_id": "marketing",
|
|
138
|
+
"outline": "- Social media\n - [ ] Instagram #social\n - TikTok\n- Newsletter\n A weekly email with what shipped."
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## What a call returns
|
|
143
|
+
|
|
144
|
+
`run` returns `{ ok, text, changed }`:
|
|
145
|
+
|
|
146
|
+
- **`text`** is for the model: send it back as the tool's result. It names nodes with their ids (`"Instagram" #instagram`), gives the new id when a new title changes one, and adds notes worth knowing, such as an id the model wrote as a title, a field the tool doesn't take, or two ideas with the same title side by side.
|
|
147
|
+
- **`ok`** is false when the call couldn't be carried out. Nothing changed, and `text` says why and what to send instead:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
No node has the id "lanch". Did you mean "Launch" #launch? read_map and find_nodes give every node's id.
|
|
151
|
+
|
|
152
|
+
"Build" #build can't go under "Frontend" #frontend, which is inside it. Move "Frontend" #frontend out of it first, or choose another parent. Nothing changed.
|
|
153
|
+
|
|
154
|
+
outline doesn't read cleanly. Fix this, and send it again:
|
|
155
|
+
- Line 2: Unknown attribute "colour". Did you mean "color"?
|
|
156
|
+
Lines count from the outline's first line. Nothing changed.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- **`changed`** says whether the map changed.
|
|
160
|
+
|
|
161
|
+
`run` never throws over a call a model gets wrong. It forgives what models often send: input as JSON text, `null` for a field left out, a number as text, tags as words separated by commas, an id with its `#`, a title only one node has instead of its id, and a field under the name models often give it (`title` for `text`, `parent` for `parent_id`). An id from before a rename still finds its node, and the id of a removed node says it was removed. A misspelled field, or a value the map can't use, changes nothing and says what it takes.
|
|
162
|
+
|
|
163
|
+
## On screen, or on a server
|
|
164
|
+
|
|
165
|
+
- **A map on screen:** `createMapTools(map)` takes what `createOrbitMap` returns, the `renderer` of an `<orbit-map>` element, or the `renderer` of `OrbitMindMap`'s ref. Each change goes through `addNode`, `updateNode`, `moveNode` and `removeNode` (see [Editing](editing.md)): it animates, Orbit announces it, and `edit` fires. `read_map` says which node the reader is looking at, and `show_node` takes them to one.
|
|
166
|
+
- **A document:** `createMapTools(markdown)` or `createMapTools(parseOrbit(markdown))`, on a server or anywhere without a map on screen. The tools change the document in place, and `serializeOrbitMarkdown(tools.document)` saves it, the way it was written apart from what changed.
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import { writeFile } from 'node:fs/promises';
|
|
170
|
+
import { createMapTools, serializeOrbitMarkdown } from '@restless-dreams/orbit';
|
|
171
|
+
|
|
172
|
+
const tools = createMapTools(markdown);
|
|
173
|
+
tools.run('add_nodes', { parent_id: 'research', outline: '- Interviews\n- Survey' });
|
|
174
|
+
await writeFile('map.md', serializeOrbitMarkdown(tools.document));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Options:
|
|
178
|
+
|
|
179
|
+
- **`readOnly: true`** offers only the tools that read (and `show_node`, on screen): for an assistant that answers questions about a map.
|
|
180
|
+
- **`readLimit`**: the most nodes `read_map` shows at once. Default 250.
|
|
181
|
+
|
|
182
|
+
## Good to know
|
|
183
|
+
|
|
184
|
+
- **Instructions.** `tools.instructions` says how to use the tools, and asks for short titles with the detail in notes, ideas in the map's language, and nodes removed only when asked: put it in the system prompt. To save the model a first call, put `tools.run('read_map').text` in the first message too.
|
|
185
|
+
- **Undo.** Keep `map.toMarkdown()` from before a request: `map.render(saved)` puts the map back, animating to it.
|
|
186
|
+
- **Asking first.** Each definition's `annotations` say whether the tool only reads (`readOnlyHint`) and whether it can overwrite or remove something (`destructiveHint`). An app can run the tools that read at once and ask before the others, as MCP clients do.
|
|
187
|
+
- **Schemas every provider reads.** The inputs use plain JSON Schema, without references or unions, and new branches come as markdown rather than nested objects, which some providers' schemas can't describe.
|
|
188
|
+
- **Newer maps.** A map written for a newer version of the format can be read but not changed, so nothing this version doesn't understand is lost.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<!-- A copy of docs/media.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
|
|
2
|
+
|
|
3
|
+
# Pictures and videos
|
|
4
|
+
|
|
5
|
+
A node's picture shows inside its planet, and a video shows its thumbnail with a play badge ([Media](format.md#media) has how to write them). Under the node in focus, "View image" and "Play video" open them in the media viewer, over the whole window.
|
|
6
|
+
|
|
7
|
+
- [The media viewer](#the-media-viewer)
|
|
8
|
+
- [Keys](#keys)
|
|
9
|
+
- [Choosing what the buttons do](#choosing-what-the-buttons-do)
|
|
10
|
+
- [From code](#from-code)
|
|
11
|
+
- [Events](#events)
|
|
12
|
+
- [YouTube and Vimeo](#youtube-and-vimeo)
|
|
13
|
+
- [Sound and autoplay](#sound-and-autoplay)
|
|
14
|
+
- [Maps that aren't interactive](#maps-that-arent-interactive)
|
|
15
|
+
- [Looks](#looks)
|
|
16
|
+
|
|
17
|
+
## The media viewer
|
|
18
|
+
|
|
19
|
+
The viewer covers the whole window, even when the map sits in a small box on a page, and follows the map's theme, light or dark.
|
|
20
|
+
|
|
21
|
+
- **Pictures** fit the window, never past twice their own size. The wheel, a pinch, a double click or tap, the `+`, `−` and `0` keys and the zoom buttons zoom around the point under the pointer or between the fingers, and a zoomed picture pans with a drag. Zooming goes to four times the fitted size, or until the picture shows at twice its own size if that's further.
|
|
22
|
+
- **Videos** start at once: files (`.mp4`, `.webm`, `.mov`) in the browser's own player, YouTube and Vimeo in theirs. The player is as big as the window allows, at the video's shape once it's known (16:9 until then, upright for YouTube Shorts). Stepping away from a video stops it.
|
|
23
|
+
- **Every picture and video of the map,** in reading order (the order the outline lists the map, a node's picture before its video): the previous and next buttons, the arrow keys and, on touch screens, a swipe step through them, starting from the one opened. A counter says where you are ("2 of 5"), and screen readers hear it.
|
|
24
|
+
- **Around them,** the node's title, the caption, and the picture's description (its alt text, which is also the picture's name for screen readers), with a link to open the picture on its own, or the video on its site.
|
|
25
|
+
- **Closing:** Esc, the close button, a click around the media or, on touch screens, a swipe down while the picture isn't zoomed. Focus goes back where it was, and the map's own keys, which rest while the viewer is open, work again.
|
|
26
|
+
|
|
27
|
+
The detail sheet shows a node's picture and video too. Its picture opens the viewer, over the sheet, and its video has an Expand button that moves it there (a video file goes on from where it was); closing the viewer goes back to the sheet.
|
|
28
|
+
|
|
29
|
+
## Keys
|
|
30
|
+
|
|
31
|
+
| Key | Does |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `←` `→` | The previous or next picture or video; on a zoomed picture, pan it |
|
|
34
|
+
| `↑` `↓` | Pan a zoomed picture |
|
|
35
|
+
| `Home` `End` | The first or last picture or video |
|
|
36
|
+
| `+` `−` `0` | Zoom in, zoom out, fit the picture to the window |
|
|
37
|
+
| `Space` | Play or pause the video |
|
|
38
|
+
| `Escape` | Close the viewer |
|
|
39
|
+
|
|
40
|
+
On a video's own controls, the arrow keys and the space bar are the player's. Inside YouTube's and Vimeo's players every key is theirs, Escape included: Tab out of the player, or click around it, to close the viewer.
|
|
41
|
+
|
|
42
|
+
## Choosing what the buttons do
|
|
43
|
+
|
|
44
|
+
The buttons ask your code first, as every button does: `onAction`, then the `action` event. What happens next:
|
|
45
|
+
|
|
46
|
+
| Your code | "View image" and "Play video" |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Nothing | Open the media viewer |
|
|
49
|
+
| `mediaViewer: false` | Open the detail sheet at the picture or video, as before the viewer |
|
|
50
|
+
| `mediaViewer: false` and `showDetailSheet: false` | Nothing: Orbit opens neither |
|
|
51
|
+
| `showDetailSheet: false` | Open the media viewer: it turns off only the sheet, for notes, embeds and details |
|
|
52
|
+
| `onAction` returns `false`, or `preventDefault()` on the `action` event | Nothing: the press is yours |
|
|
53
|
+
|
|
54
|
+
The detail sheet's picture and Expand button are presses of "View image" and "Play video" too: your code sees them first, and when it takes one the sheet closes, so what you open is in front. With `mediaViewer: false`, the sheet's picture is only a picture.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
createOrbitMap(element, markdown, {
|
|
58
|
+
onAction: ({ node, action }) => {
|
|
59
|
+
if (action === 'image') {
|
|
60
|
+
openMyLightbox(node.image.src);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## From code
|
|
70
|
+
|
|
71
|
+
A page can open the viewer itself, to show a picture big or play a video on a screen of its own:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
map.openMedia('summit'); // a node's picture, or its video if it has no picture
|
|
75
|
+
map.openMedia('trail', { kind: 'video', autoplay: false }); // its video, ready to play
|
|
76
|
+
map.openMedia('trail', { kind: 'video', startAt: 42 }); // its video, playing from 0:42
|
|
77
|
+
map.playMedia();
|
|
78
|
+
map.seekMedia(42);
|
|
79
|
+
map.pauseMedia();
|
|
80
|
+
map.unmuteMedia(); // from a press: the sound of a video a browser started muted
|
|
81
|
+
map.closeMedia();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Method | Does |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `openMedia(nodeId, { kind, autoplay, startAt })` | Shows the node's picture (`kind: 'image'`, the default when it has one) or video (`kind: 'video'`), opening the viewer if it's closed. A video starts playing unless `autoplay` is `false`, from its start or `startAt` seconds in (YouTube and Vimeo start at the whole second). Returns `false` when the map has no such node, or the node no such picture or video. |
|
|
87
|
+
| `closeMedia()` | Closes the viewer. |
|
|
88
|
+
| `playMedia()`, `pauseMedia()` | Plays or pauses the video it shows. Returns `false` when it shows no video it can drive. |
|
|
89
|
+
| `seekMedia(seconds)` | Moves the video to `seconds` from its start. Returns `false` when it shows no video it can drive. |
|
|
90
|
+
| `unmuteMedia()` | Turns on the sound of the video it shows. Browsers allow it only from a press (see [Sound and autoplay](#sound-and-autoplay)). Returns `false` when it shows no video it can drive. |
|
|
91
|
+
|
|
92
|
+
`openMedia` opens the viewer whatever `mediaViewer`, `showDetailSheet` and `interactive` say: those choose what the buttons do. Files, YouTube and Vimeo can be driven; a video on any other site plays in a frame, with its own controls only. `<orbit-map>` has the same six methods.
|
|
93
|
+
|
|
94
|
+
This is what an audience's screen does while someone presents from a phone: it opens what the presenter opens, where the presenter's video is, and follows the presenter's player, and tells the phone how the video is going.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const map = createOrbitMap(element, markdown, { interactive: false, showControls: false });
|
|
98
|
+
|
|
99
|
+
presenter.addEventListener('message', ({ data }) => {
|
|
100
|
+
if (data.type === 'media') {
|
|
101
|
+
map.openMedia(data.id, { kind: data.kind, autoplay: data.playing, startAt: data.seconds });
|
|
102
|
+
} else if (data.type === 'play') {
|
|
103
|
+
map.playMedia();
|
|
104
|
+
} else if (data.type === 'pause') {
|
|
105
|
+
map.pauseMedia();
|
|
106
|
+
} else if (data.type === 'seek') {
|
|
107
|
+
map.seekMedia(data.seconds);
|
|
108
|
+
} else if (data.type === 'close') {
|
|
109
|
+
map.closeMedia();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
map.on('media-state', ({ node, playing, time }) => {
|
|
114
|
+
presenter.postMessage({ type: 'state', id: node.id, playing, time });
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Events
|
|
119
|
+
|
|
120
|
+
| Event | DOM event | Detail |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `media-open` | `orbit-media-open` | `{ node, kind, index, total }`: the viewer opened, or stepped, on a node's picture (`kind: 'image'`) or video (`'video'`), `index` of `total` in reading order, from 0 |
|
|
123
|
+
| `media-close` | `orbit-media-close` | `{ node, kind, index, total }`: the viewer closed, showing that |
|
|
124
|
+
| `media-state` | `orbit-media-state` | `{ node, playing, time, duration, ended, muted }`: the video started, paused, moved to another time, ended or was muted, at once; while it plays, its time a few times a second. `time` and `duration` are in seconds; `duration` is `null` until it's known |
|
|
125
|
+
|
|
126
|
+
`onMediaOpen`, `onMediaClose` and `onMediaState` receive the same, and so do the React props of the same names. The DOM events bubble from the map's container and out of `<orbit-map>`.
|
|
127
|
+
|
|
128
|
+
## YouTube and Vimeo
|
|
129
|
+
|
|
130
|
+
YouTube videos play in YouTube's privacy-enhanced player (`youtube-nocookie.com`), and Vimeo videos in Vimeo's with `dnt=1`, which tracks no sessions. Links to watch pages, `youtu.be`, Shorts, Vimeo channels, groups, showcases and unlisted videos all work; Orbit frames their players, never their pages, which refuse to be framed.
|
|
131
|
+
|
|
132
|
+
- **The referrer:** the players' frames send the page's origin, never its address (`referrerpolicy="strict-origin-when-cross-origin"`). YouTube's player won't play without knowing the site it's on (its "Error 153"), and a page that sends no referrer (`Referrer-Policy: no-referrer`) still gives its origin to these frames. A page opened from a file has no origin to give, so YouTube's videos don't play there.
|
|
133
|
+
- **Driving them:** `playMedia`, `pauseMedia` and `seekMedia` reach the players through the messages their own JavaScript APIs send (YouTube's `enablejsapi=1`, with the page's `origin`; Vimeo's player.js messages), and `media-state` follows what the players say. Nothing is loaded from either site but the player itself.
|
|
134
|
+
- **Other sites:** a video on any other site plays in a frame, as the detail sheet shows an embed, with only its own controls: `playMedia` and its kin return `false` for it, and it reports no `media-state`.
|
|
135
|
+
|
|
136
|
+
## Sound and autoplay
|
|
137
|
+
|
|
138
|
+
Browsers let a video start with sound only once the reader has interacted with the page. A press of "Play video", the arrows or a swipe counts, so those videos play with sound.
|
|
139
|
+
|
|
140
|
+
A video opened from code on a page nobody has touched yet, such as an audience's screen, may not be allowed to. A video file then starts muted, and its `media-state` says `muted: true`. YouTube's and Vimeo's players decide for themselves: they start muted, or wait for a press. Screens that should play sound from the start can allow it: a kiosk Chrome with `--autoplay-policy=no-user-gesture-required`, or an Android `WebView` with `setMediaPlaybackRequiresUserGesture(false)`.
|
|
141
|
+
|
|
142
|
+
Elsewhere, offer a button for the sound while `media-state` says the video is muted, or isn't playing when it should. Its press counts as interacting with the page, so `unmuteMedia()` and `playMedia()` from its click play the video with sound, YouTube's and Vimeo's included. The viewer is a modal `<dialog>` in the top layer, and a modal dialog makes the rest of the page inert, popovers included: show the button in a `<dialog>` of its own with `showModal()`, after the viewer opens, so it's on top and can be pressed.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
soundButton.addEventListener('click', () => {
|
|
146
|
+
map.unmuteMedia();
|
|
147
|
+
map.playMedia();
|
|
148
|
+
soundDialog.close();
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Maps that aren't interactive
|
|
153
|
+
|
|
154
|
+
On a map with `interactive: false`, a still picture, the viewer is the page's to drive. It shows the picture or video with its title and caption, but no buttons, no counter and no controls on videos, and it takes no input of its own: Esc and clicks don't close it, and keys and presses reach the page, so a presentation's own keys keep working. Open, close and play it with the methods above.
|
|
155
|
+
|
|
156
|
+
## Looks
|
|
157
|
+
|
|
158
|
+
The viewer takes its colors from the detail sheet's custom properties: `--orbit-sheet-background` behind the media (blurred by `--orbit-sheet-blur` where it's see-through, as in the glass themes), `--orbit-sheet-text` and `--orbit-sheet-muted-text` for the title, caption and counter, and `--orbit-sheet-shadow` under a video. Its buttons are the map's controls (`--orbit-control-*`, `--orbit-focus-ring`). See [Theming](theming.md#custom-properties).
|
|
159
|
+
|
|
160
|
+
With reduced motion (`prefers-reduced-motion: reduce`), zooms and steps happen at once instead of gliding and fading.
|