Mind map из Markdown-списка: как Open MindMap стыкует потоковый вывод LLM с живой SVG-картой

Потоковый ответ модели можно вести прямо в интерфейс mind map — если единственным источником правды остаётся обычный Markdown-список с отступами, без отдельного конвейера постобработки. разбор Open MindMap Разработчик Open MindMap (@amery2010) в свежем build-log на Dev.to показывает, почему такой формат превращает LLM streaming в побочный эффект архитектуры, а не отдельную «AI-фичу» в коде компонента.
Почему потоковый вывод LLM «выпадает почти бесплатно» из формата данных
Open MindMap — React-компонент, где карта хранится не в проприетарном бинарнике и не в WYSIWYG-хранилище, а в plain indented Markdown-like list. Автор прямо связывает это с заголовком поста: indented list превращается в живую SVG mind map, и «LLM streaming fall out for free».
Для AI-assisted UX смысл простой: модель естественно выдаёт иерархию списком с отступами — токен за токеном. Частичный буфер при этом остаётся валидным Markdown: неполный список — это просто список покороче. Компоненту не нужно знать, что за строкой стоит LLM: любой вызывающий код, который на каждом тике передаёт растущую строку через setMarkdown, получает тот же эффект «растущей» карты.
Числовых замеров latency или стоимости streaming в материале нет — только качественное обещание «almost free» через дизайн, а не через бенчмарк.
Архитектура text → tree → layout → SVG для diff-friendly AI-генерации
Архитектуру автор описывает одной цепочкой:
text → tree → layout → SVG
Каждый этап — pure и однонаправленный, и каждый round-trips обратно в text. Симметрия parse ↔ serialize важна для diff-friendly карт, программной генерации и того же streaming: изменение в git — одна строка diff, а не непрозрачная бинарная дельта.
Модель данных MindMapData держит id, text, опционально children, remark, taskStatus и plugin-поля (tags, anchorId, crossLinks, collapsed и др.). Координаты, ширины и цвета в модели не живут — позиция и стиль выводятся при layout и render. Для vibe coding это удобная граница: LLM и человек правят текст; визуал пересчитывается.
Рендер — pure SVG (не canvas, без внешней graph/layout library): чёткий zoom, CSS-темизация через ~30 переменных, бесплатный export. Tradeoff — много DOM-узлов на больших деревьях; folding смягчает, но полноценная виртуализация SVG-дерева автором названа отдельным проектом.
Демо: mindmap.u14.app. Код — github.com/u14app/mindmap, npm-пакет @xiangfa/mindmap, лицензия Apache-2.0.
Парсер O(n) и плагины: неполный LLM-вывод не ломает дерево
Парсер — однопроходный stack по глубине отступа; маркеры -, *, + снимаются. Сложность O(n) по числу строк, без backtracking. Несколько top-level строк через пустую строку дают forest of trees на одном canvas.
После структурного parse работают два слоя синтаксиса. Line-level markers — теги вроде #framework, task [x]/[ ], remark >, anchor {#launch}, cross-link -> {#launch}. Inline formatting — lazy parseInlineMarkdown только при рендере узла (off-screen и collapsed не токенизируются).
Семь встроенных плагинов (tags, folding, cross-links, LaTeX, dotted lines, multi-line, frontmatter) включены по умолчанию и tree-shakeable; плагин = пара hooks на parse и render. Round-trip API: parseMarkdownList / toMarkdownList и мультикорневой вариант — встроенный text editor переключает visual map ↔ raw Markdown без lossy conversion.
Для потокового сценария критично второе свойство парсера: он cheap и total — не падает на неполном вводе. Полустрока становится узлом с коротким text до следующего тика; не нужна state machine «дождаться complete element».
setMarkdown на каждом чанке: паттерн потребителя LLM-stream
Типичный потребитель потока в посте выглядит так:
let buffer = "";
for await (const chunk of llmStream) {
buffer += chunk;
mindMapRef.current?.setMarkdown(buffer); // re-parse partial text every tick
}
На каждом тике — O(n) re-parse всего buffer и свежее дерево в React. Звучит расточительно, но автор опирается на два механизма «гладкости» без цифр на бумаге:
- Парсер дешёвый и толерантный к обрывам — UI не мигает ошибками валидации на середине слова.
- Diff между тиками маленький: reconciliation и SVG re-layout трогают только изменившееся; карта растёт, а не перерисовывается целиком.
Встроенная AI input bar шлёт запросы на любой OpenAI-compatible endpoint; streaming для неё не special-cased — тот же setMarkdown(growing string). Отсюда и тезис: компонент «не обязан знать, что существует LLM», а интеграция с агентом или IDE сводится к подаче растущего текста.
Оговорка безопасности из поста: AI bar отправляет API key из браузера — ок для local prototyping; в production автор рекомендует proxy, чтобы ключ оставался server-side.
Экспорт, layout-caveats и production-ограничения AI-бара
Export покрывает SVG (walk rendered tree + inline <style>), PNG через SVG → canvas high DPI, Markdown через toMarkdownList / exportToOutline. Для AI-воркфлоу полезно, что и визуал, и текст остаются производными одного текстового источника.
Автор честно перечисляет caveats, которые не отменяют streaming, но влияют на production:
- измерение ширины текста и поздние web fonts → layout bugs после first paint;
- большие деревья → потолок DOM-узлов;
- cross-links — второй слой рёбер, routing без пересечений «genuinely unsolved here»;
- browser-side API keys → нужен proxy в реальных деплоях.
Итог для разработчика AI-интерфейсов: Open MindMap — не привязка к конкретному вендору LLM, а архитектурный паттерн plain-text source of truth + incremental re-parse. Потоковая mind map из модели становится естественным следствием формата, если вы готовы платить пересчётом дерева на каждом чанке — без обещанных в посте количественных бенчмарков, зато с открытым кодом и демо для проверки на своих промптах.
Источники
- How I built a mind map that's just a Markdown list (and why that makes AI streaming almost free) — @amery2010, Dev.to, опубликовано 19 июня 2026
- Демо: mindmap.u14.app
- Репозиторий: github.com/u14app/mindmap
- npm:
@xiangfa/mindmap(Apache-2.0)