ГлавнаяБлогFlowChat: как ИИ строит интерфейсы в реальном времени
AI / Нейросети

FlowChat: как ИИ строит интерфейсы в реальном времени

Узнайте, как ИИ может не просто отвечать текстом, а создавать живые интерфейсы: игры, темы, фоны и целые страницы. Попробуйте сами!

Al
Редакция Algolitalgolit.ru
12 мин чтения29 июля 2026 г.

Что такое FlowChat и зачем это нужно?

Представьте, что вы пишете ИИ: «Сделай игру «Крестики-нолики» — и тут же получаете рабочее поле, по которому можно кликать. Говорите «Смени тему на Барби» — и весь интерфейс становится розовым. Просите «Добавь звёздное небо на фон» — и за сообщениями появляется анимированный canvas. Это не фантастика, а реальный проект FlowChat, который я собрал за несколько недель. В этой статье я расскажу, как он устроен, и покажу код, который вы сможете адаптировать под свои задачи.

Почему обычный AI-чат — это скучно

Обычный AI-чат работает так: модель возвращает markdown, клиент рендерит его как текст. Просто, предсказуемо, скучно. В FlowChat модель возвращает сырой HTML с CSS и JavaScript, а клиент инжектирует его прямо в DOM через стриминговый протокол на основе нативного шаблонного браузера. Это одно изменение полностью меняет опыт: вы не читаете об игре — вы в неё играете. Вы не читаете о цветовой палитре — вы находитесь внутри неё.

Что можно делать с FlowChat

Игры

Попросите ИИ построить «Крестики-нолики» — и получите игровое поле, клик-ход, AI-противника и определение победителя. Запросите «Connect 4», «Змейку» — игра рендерится внутри сообщения агента как форма. Каждый ход отправляется модели, которая обновляет только изменившиеся ячейки.

Темы

Скажите «Смени на тему Барби» — модель инжектирует CSS-переопределения, и весь интерфейс становится розовым: сообщения, границы, кнопки, поле ввода. «Сделай в стиле Оппенгеймера» — тёмные сепия тона и массивная типографика. Боковая панель и топбар остаются нетронутыми.

Фоны

«Добавь звёздное небо» — анимированный canvas появляется за сообщениями. «DVD-заставка» — логотип прыгает по окну чата. «Используй космическое изображение» — фон заполняется картинкой. Всё это живёт в изолированном слое, не перекрывая UI.

Полный захват интерфейса

Однажды я попросил сделать страницу похожей на Wikipedia. ИИ заменил поле ввода на ссылки. Клик по ссылке отправлял форму модели, которая генерировала новую статью, заменяя содержимое чата. Я читал о Римской империи в чат-приложении, которое сам собрал в воскресенье днём.

Технический стек

Всё работает на Edge-инфраструктуре Cloudflare. Никакого традиционного сервера, Node.js процесса или базы данных. Cloudflare Workers выполняет TypeScript на каждом запросе с холодным стартом менее 50 мс. Один файл обрабатывает роутинг, аутентификацию, лимиты, WebSocket и стриминг LLM.

Cloudflare Durable Objects — ключевая часть. Каждая комната чата — это отдельный Durable Object: stateful actor со своей SQLite-базой, очередью в памяти и WebSocket-соединениями. Когда вы и друг открываете один URL чата, вы оба подключаетесь к одному DO. Синхронизация не требует дополнительной разработки — это заложено в архитектуру.

Каждый DO хранит:

  • Полную историю сообщений LLM в SQLite
  • Записи сессий клиентов
  • Очередь ожидающих промптов (макс. 5)
  • Состояние лимитов по браузеру/IP
  • Индекс форков для снэпшотов только для чтения

Hibernatable WebSockets держат соединения активными без постоянного удержания DO. Cloudflare автоматически обрабатывает ping/pong. DO просыпается, когда приходит сообщение, и засыпает между ними.

better-auth отвечает за опциональную аутентификацию. Если не настроить, приложение открыто всем. Если настроить, доступны Google, GitHub и email/password с ролевым доступом (admin, dev, chat, view, blocked).

Inception Labs Mercury-2 — модель, генерирующая ответы. Это диффузионная языковая модель, а не авторегрессионная, поэтому она генерирует иначе, чем GPT или Claude. На практике она быстрая и хорошо понимает нужный формат HTML.

Протокол стриминга

Самая интересная часть — протокол. AI не может просто выгружать сырой HTML в поток ответа. Один ответ может обновлять три разные части страницы независимо. Ход в «Крестиках-ноликах» должен обновить одну ячейку, а не перерисовывать всю доску. Фоновая анимация не должна влиять на боковую панель. Приватное сообщение одному игроку не должно появляться у другого.

Я построил протокол на основе разделителей. Модель оборачивает каждое DOM-обновление в структурированный конверт:

PpqUtcLGQdYN4oqc:BODY_START
<template for="/chat/append-message">
<div class="message message-user" data-client-id="1">Давай сыграем в крестики-нолики</div>
<div class="message message-agent message-full-width" id="msg-1">
  <!-- вся доска -->
</div>
<?marker name="/chat/append-message"?>
</template>
PpqUtcLGQdYN4oqc:BODY_END

Атрибут for на шаблоне указывает на именованный маркер в DOM. Клиентский рантайм находит в документе инструкции обработки с совпадающими именами и заменяет их содержимым шаблона. Хирургически, не трогая ничего другого.

Один ответ AI может содержать несколько сообщений, разделённых разделителем SPLIT_MESSAGE. Так модель может отправить публичное подтверждение всем пользователям и одновременно приватное сообщение только одному игроку, используя инструкции SERVER_PROPS, которые сервер удаляет перед отправкой через WebSocket.

Вся система построена на двух полифиллах, реализующих спецификацию Dynamic Partial Update, которая внедряется в Chrome.

Написание системного промпта

Чтобы AI стабильно генерировал валидный HTML в этом формате, потребовалось много итераций. Финальный системный промпт — около 300 строк, больше похожий на API-контракт, чем на промпт.

Он содержит точные hex-значения всех CSS-переменных дизайн-системы, чтобы модель правильно использовала var(--accent), а не угадывала цвета. Правила для border-radius, теней, таймингов анимаций. Паттерн асинхронной загрузки Chart.js и d3 через CDN, потому что модель постоянно вызывала new Chart() до загрузки библиотеки.

Самая большая ошибка: модель размещала маркер append-message внутри контейнера приложения вместо того, чтобы ставить его после. Каждое следующее сообщение инжектировалось внутрь игровой доски. Исправил примером в промпте:

<!-- НЕПРАВИЛЬНО: маркер внутри div, следующее сообщение вставится сюда навсегда -->
<div id="ttt-app-1">
  ...доска...
  <?marker name="/chat/append-message"?>
</div>

<!-- ПРАВИЛЬНО: маркер после закрытия всех div -->
<div id="ttt-app-1">
  ...доска...
</div>
<?marker name="/chat/append-message"?>

Примеры с ошибками с явными комментариями полезнее, чем документация только с правильными вариантами. Модель должна знать, как выглядит сбой, а не только счастливый путь.

Также я выяснил, что диффузионные модели (Mercury-2) требуют немного иного промптинга, чем авторегрессионные. Ответы ощущаются не как печатание на машинке, а как материализация контента. Это отлично сочетается с данным сценарием.

Многопользовательский режим по умолчанию

Каждый URL чата является общим. Откройте одну ссылку в двух вкладках браузера — обе будут получать ответы AI через WebSocket в реальном времени. Каждый клиент получает уникальный ID. Пузырьки пользователей окрашены по клиенту.

LLM знает ID каждого клиента:

[1]: Я хочу угадать секретное слово
[2]: Я хочу дать подсказку

Модель может ответить одним сообщением, видимым обоим игрокам, и вторым, содержащим секрет, видимым только клиенту 1. Маршрутизация на сервере, удаление из WebSocket-пейлоадов перед отправкой не тому браузеру.

Я не добавлял специальной многопользовательской логики. Архитектура Durable Object делает это естественно: каждый клиент подключается к одному экземпляру DO, у которого есть все WebSocket-соединения. Когда LLM отвечает, DO транслирует ответ всем.

UI: чистый CSS

Чистый CSS. Никаких фреймворков, Tailwind или библиотек компонентов. Шрифт Inter загружается неблокирующе, глубокая тёмно-синяя палитра (#06091a до #101630), акцентный цвет перинкл индиго (#5b6ef5).

Оболочка приложения состоит из боковой панели и основной области с топбаром. Боковая панель и топбар всегда физически непрозрачны. Область чата — единственная зона, где могут рендериться темы и фоны. Это предотвращает случайное перекрытие навигации космическим фото, что иначе происходило постоянно.

.chat-viewport {
  position: relative;
  isolation: isolate;
}
#fc-bg-layer {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none !important;
}
.chat {
  position: relative;
  z-index: 2;
}

Фоновый слой на z-index 0. Сообщения чата на z-index 2. Боковая панель и топбар — отдельные элементы вне области просмотра. Структурное разделение эффективнее, чем попытки принудительно задать стили через !important и MutationObserver, которые я сначала попробовал и которые вызвали бесконечный цикл, заморозивший страницу. Урок усвоен.

Индикатор набора текста, оптимистичные пузырьки пользователей, пружинные анимации появления сообщений. Кнопка отправки имеет свечение. Мелочи, но они складываются.

Честные болевые точки

Ошибка размещения маркера заняла два дня, потому что проблема была невидима до второго сообщения. Первое всегда выглядело правильно.

Войны с фоновым контейнером длились около недели. Я пробовал CSS !important, затем MutationObserver, затем JS-блокировку фона. Всё ломало что-то ещё. Правильный ответ — структурный: переместить фоновый слой внутрь области чата, чтобы он физически не мог выйти.

Загрузка скриптов CDN подводит каждое AI-приложение. Модель пишет код, вызывающий API Chart.js до загрузки библиотеки. Решение — научить модель использовать polling:

function init() {
  if (typeof Chart === 'undefined') {
    setTimeout(init, 50);
    return;
  }
  // безопасно использовать Chart
}
init();

Этот паттерн теперь встроен в системный промпт и работает надёжно.

Ошибка с URL формы была досадной. Атрибут action в app.html был c/CHAT_ID/prompt (относительный), а не /c/CHAT_ID/prompt (абсолютный). При свежей загрузке путь резолвился правильно. После редиректа — нет. Каждый промпт отправлялся на /c/c/CHAT_ID/prompt и получал 404. Я заметил это по логам сервера и добавил глобальный перехватчик отправки форм, который нормализует относительные URL, как страховку для AI-сгенерированных форм.

Деплой

Всё работает на бесплатном тарифе Cloudflare. Одна команда:

npx wrangler deploy --env public

Никакого Docker, сервера или настройки базы данных. Cloudflare автоматически управляет масштабированием, гибернацией WebSocket, глобальным распределением и SQLite-хранилищем внутри каждого Durable Object.

Секреты (например, API-ключ) хранятся через Wrangler:

npx wrangler secret put INCEPTION_API_KEY --env public

Они никогда не попадают в код или систему контроля версий.

Попробуйте сами

Живая версия: https://flowchat-public.varshithvh.workers.dev
Исходный код: https://github.com/Varshithvhegde/flowchat

Откройте новый чат. Напишите что угодно. Попросите построить игру, сменить тему, добавить фон или сделать страницу похожей на что-то совершенно другое. Это работает.

Практический вывод

Вы можете использовать эту архитектуру в своих проектах: возьмите Durable Objects для синхронизации состояний, реализуйте протокол частичных обновлений через маркеры и используйте диффузионную LLM для генерации интерфейсов. Начните с малого: попробуйте заменить обычный ответ AI на интерактивный виджет, например, форму или кнопку. Адаптируйте системный промпт под свой дизайн. И помните: структурное разделение контейнеров — ваш главный друг.

#FlowChat#AI-интерфейсы#реальное время#Cloudflare Workers#Durable Objects
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →