Skip to main content

Архитектура Builder-web

Расширенное описание внутреннего устройства визуального редактора, потоков данных, состояния, валидации, превью, expression‑DSL, подсветки ошибок, системы моков для источников данных и AI‑ассистента.


🎯 Цель страницы

Эта страница подробно объясняет архитектуру @lowcode/builder-web, включая:

  • структуру каталогов и роль каждого модуля;
  • устройство центрального состояния редактора (EditorState);
  • формирование и обновление DSL‑дерева;
  • работу панелей PropertiesPanel / StatePanel / EventHandlersPanel / DataSourcesPanel / AiAssistPanel;
  • механизм expression‑валидации (syntax + semantic);
  • превью‑pipeline (DSL → AST → TSX → JS → runtime-core);
  • механизм подсветки ошибок и связь Canvas ↔ PreviewPanel;
  • интеграцию с @lowcode/runtime-core и devtools‑событиями;
  • поддержку моков (mock) для DataSource в DSL и превью;
  • интеграцию с AI‑оркестратором и панелью AI‑ассистента;
  • принципы проектирования всего редактора.

Документ предназначен как глубокое архитектурное описание.


1. Общая роль Builder-web

builder-web — это визуальная IDE для работы с DSL‑моделью приложения. Он объединяет:

  • графический редактор UI (Canvas);
  • редактор свойств (PropertiesPanel v2);
  • редактор состояния (StatePanel);
  • редактор событий и действий (EventHandlersPanel);
  • редактор источников данных и моков (DataSourcesPanel);
  • панель AI‑ассистента (AiAssistPanel) поверх Canvas/Preview;
  • runtime‑превью, исполняющее скомпилированный DSL через @lowcode/runtime-core;
  • подсветку ошибок DSL и expression‑валидации;
  • панель инспектора runtime‑состояния и событий;
  • историю действий с Undo/Redo и авто‑сохранением, чтобы любые правки (включая AI‑операции) можно было откатить горячими клавишами (Cmd/Ctrl+Z, Cmd/Ctrl+Y).

Builder-web работает полностью локально в браузере:

  • структура DSL валидируется через @lowcode/dsl;
  • выражения валидируются как по синтаксису, так и по семантике (DSL + AST);
  • React‑код генерируется через @lowcode/dsl-compiler;
  • TSX‑бандл исполняется через общий движок @lowcode/runtime-core;
  • Canvas, панели и превью синхронно отражают состояние одного и того же DSL;
  • для DataSource в превью используются только моки (mock‑конфигурация в DSL), реальные HTTP‑запросы из builder-web не выполняются;
  • AI‑ассистент работает поверх текущего DSL, но не генерирует код напрямую — он возвращает список декларативных операций (AiOperation), которые применяются к AppSchema на стороне оркестратора.

Основные зависимости:

  • @lowcode/dsl — структура DSL, валидация, ExpressionValue, DataSource + DataSource.mock;
  • @lowcode/dsl-compiler — AST, expression‑анализ, TSX‑генерация (учитывает список dataSources и их mock‑конфигурацию);
  • @lowcode/runtime-core — исполняющий движок (bundle → modules → RuntimeInstance, в т.ч. default executor с поддержкой моков);
  • @lowcode/ui-kit — UI‑компоненты редактора (панели, карточки, LayoutShell и т.п.);
  • @lowcode/ai-orchestrator — формирование промпта, строгий JSON‑формат операций и применение AiOperation к DSL.

Исторически превью использовало @babel/standalone напрямую, но в текущей архитектуре вся ответственность за компиляцию и исполнение бандла вынесена в @lowcode/runtime-core, а builder-web концентрируется на UX и настройке DSL (включая моки и интеграцию с AI).


2. Структура каталога builder-web

apps/builder-web/src/
├─ api/ # HTTP‑клиент для @lowcode/api
├─ ai/ # хуки и утилиты для AI‑ассистента
├─ auth/ # AuthContext, AuthGate, AuthScreen (логин/регистрация)
├─ account/ # AccountContext и хуки профиля/подписки
├─ components/ # Canvas, PreviewPanel, LayoutShell, панели редактора
├─ expressions/ # validateExpression, редактор выражений (CodeMirror), контексты
├─ state/ # EditorState, централизованная логика DSL
├─ data/ # шаблоны DSL
├─ pages/ # страницы SPA (home/editor/...)
├─ styles/ # глобальные стили, Tailwind-кастомизация
├─ types.ts # общие типы для редактора
├─ App.tsx # корневой компонент SPA
└─ main.tsx # точка входа Vite/React

2.1. state/ — EditorState

Здесь живёт центральное состояние редактора — EditorState:

  • текущий app: AppSchema | null;
  • activePageId: string | null — активная страница;
  • activeBlockId: string | null — активный custom‑блок (в режиме Blocks);
  • isEditingBlocks: boolean — флаг режима редактирования блоков;
  • selectedNodeId: string | null — выбранный компонент в Canvas;
  • issueNodeIds: string[] — ID компонент с ошибками (DSL/выражения/runtime);
  • состояние панелей (раскрытые секции, выбранные вкладки и т.п.);
  • вспомогательные флаги/поля (загрузка, сохранение, источники шаблонов и т.п.).
  • внутренняя история изменений DSL (стеки past/future) для Undo/Redo, синхронизированная с автосохранениями и AI‑операциями.

Также в state/ находятся:

  • хук useEditorState() (контекст редактора);
  • pure‑функции для работы с DSL‑деревом (поиск, обновление, вставка/удаление);
  • хелперы для работы с app.dataSources и mock‑конфигурацией (создание/удаление/обновление источников и их моков).

2.2. components/

Основные визуальные блоки:

  • LayoutShell.tsx — каркас редактора (верхний бар, левая колонка с Canvas, правая с панелями, нижнее превью). В хэдэре отображаются кнопки Undo/Redo, переключатель темы, кнопка Settings (открывает общую модалку настроек с управлением авто‑сохранением и переходом в Account Settings);
  • Canvas.tsx — дерево компонентов DSL (визуализация ComponentNode), поддерживает базовые layout-компоненты (Container/Grid/Repeat/If);
  • PreviewPanel.tsx — превью на базе @lowcode/runtime-core;
  • PropertiesPanel.tsx — PropertiesPanel v2 (динамически строится по ComponentDefinition);
  • BlocksPanel.tsx — управление custom‑блоками (создание/переименование/props) в режиме Blocks;
  • StatePanel.tsx — редактор appState / page.state;
  • EventHandlersPanel.tsx — редактор событий и Actions;
  • DataSourcesPanel.tsx — редактор источников данных и mock‑конфигурации;
  • RuntimeStateInspector.tsx — панель просмотра текущего RuntimeSnapshot;
  • AiAssistPanel.tsx — панель AI‑ассистента над Canvas/Preview;
  • SettingsModal.tsx — централизованные настройки редактора (auto-save интервал, переход в Account Settings);
  • MediaLibrary/ — модальное окно для управления attachments проекта:
    • MediaLibraryModal.tsx — основная модалка с тремя режимами (browse/details/upload);
    • MediaLibraryGrid.tsx — сетка превью файлов с фильтрацией по MIME type;
    • MediaLibraryDetails.tsx — детальный просмотр выбранного attachment;
    • MediaLibraryUpload.tsx — форма загрузки файлов в S3 или добавления ссылок;
    • MediaLibraryHeader.tsx — верхняя панель с поиском и переключением режимов.
  • вспомогательные компоненты (toolbar'ы, списки, формы ввода и т.п.).

2.3. expressions/

Слой локальной syntax-валидации выражений в стиле:

validateExpressionSyntax(expression: string): {
isValid: boolean;
errors: string[];
};

Дополнительно здесь находится контекст редактора выражений (ExpressionEditorContext) и хелперы для сборки контекста (variables, dataSources, event).

Используется для:

  • проверки полей ввода выражений в PropertiesPanel/StatePanel;
  • быстрой подсветки синтаксических ошибок до запуска dsl-compiler;
  • дополнения диагностик, которые приходят из validateExpressionsInAstApp.

2.4. api/

API‑клиент для доступа к сервису @lowcode/api:

  • запрос списка проектов;
  • загрузка/сохранение версии проекта;
  • запрос на валидацию DSL;
  • вызов AI‑ассистента через POST /ai/assist (callAiAssist).

С появлением клиентских аккаунтов client.ts также отвечает за:

  • регистрацию/логин (/auth/register, /auth/login, /auth/logout, /auth/refresh);
  • хранение access/refresh токенов в localStorage (lowcode:auth);
  • автоматическую подстановку Authorization: Bearer <token> для всех запросов;
  • silent-refresh access‑токена при 401, используя refresh‑токен;
  • получение профиля аккаунта (/account/profile) и обновление пользовательских/клиентских настроек.

Это означает, что любой код редактора, который вызывает listProjects, loadProject, saveProject и т.п., уже получает client-scoped данные без необходимости передавать clientId. Единственное требование — перед началом работы вызвать registerClientAccount или login, чтобы токены попали в client.ts.

2.5. ai/

Слой интеграции с AI‑оркестратором:

  • useAiAssist.ts — хук верхнего уровня для сценария AI‑ассистента;
  • утилиты/типы для общения с backend‑API;
  • именно здесь задаются дефолтные значения AI‑провайдера и модели (см. раздел про AI‑ассистент).

2.6. auth/ и account/

Новые директории отвечают за всю клиентскую работу с аккаунтами:

  • auth/ содержит AuthContext, AuthGate и AuthScreen.
    AuthProvider хранит текущую AuthSession, автоматически обновляет access‑токен по refresh‑токену и предоставляет login, register, logout, refreshSession.
    AuthGate блокирует загрузку редактора до тех пор, пока не будет либо подтверждена сессия, либо показана AuthScreen.
    AuthScreen реализует UI логина и создания клиента/воркспейса (slug + владелец) и использует UI‑kit.

  • account/ содержит AccountContext, который поверх AuthContext загружает профиль пользователя, клиента и историю подписок (/account/profile, /account/subscription, /account/subscription/history) и предоставляет методы updateProfile, changePassword, updateClientSettings, refreshSubscription.

  • Компонент AccountSettingsModal размещён в components/ и использует AccountContext для построения UI личного кабинета (вкладки Profile/Workspace/Subscription + logout).

  • App.tsx подключает эту инфраструктуру: слева остаётся редактор, а в правой части хэдера через LayoutShell.headerRightExtras выводится кнопка аккаунта с бейджем тарифа. При клике открывается AccountSettingsModal.

Тестовый аккаунт по умолчанию

Для локальной разработки заведён клиент default-client с владельцем:

Email:    admin@default.local
Password: DevPass123

Эти данные подходят для входа в builder-web сразу после запуска backend’а и дают доступ ко всем мигрированным проектам.


3. EditorState — центральная модель данных

EditorState — ядро builder-web. Он объединяет:

  • DSL‑модель (app: AppSchema);
  • текущий UI‑контекст (активная страница, выбранный компонент);
  • список проблемных компонент (issueNodeIds);
  • настройки редактора (например, какой режим отображения включён);
  • конфигурацию источников данных и моков (app.dataSources + DataSource.mock);
  • точки интеграции с превью (через PreviewPanel);
  • интеграцию с AI‑ассистентом (передача app, projectId, pageId, hints).

3.1. DSL‑дерево как источник истины

DSL полностью хранится в памяти в виде AppSchema:

interface AppSchema {
id: string;
name: string;
version: 1;
pages: PageSchema[];
appState?: AppStateSchema;
dataSources?: DataSource[];
eventHandlers?: EventHandler[];
// ...
}

Любое изменение в интерфейсе (поменяли текст кнопки, добавили состояние, настроили обработчик события или mock для DataSource, применили операции AI‑ассистента) приводит к иммутабельному изменению AppSchema:

  1. создаётся новая структура AppSchema с применённым патчем;
  2. запись производится в EditorState;
  3. Canvas, панели и PreviewPanel получают новый app и перерисовываются.

3.2. Иммутабельные обновления

Обновления DSL реализованы как pure‑функции:

setApp((prev) =>
updateComponentNode(prev, nodeId, (node) => ({
...node,
props: {
...node.props,
text: 'New label',
},
})),
);

и аналогично для источников данных:

setApp((prev) =>
updateDataSource(prev, dataSourceId, (ds) => ({
...ds,
mock: {
...ds.mock,
enabled: true,
mode: 'static',
value: { items: demoUsers },
},
})),
);

или при применении результата AI‑ассистента:

if (response.updatedAppSchema) {
setApp(response.updatedAppSchema);
}

Это гарантирует:

  • отсутствие неожиданных сайд‑эффектов;
  • корректный re-render в React;
  • прозрачную историю изменений (в будущем можно будет навинтить undo/redo).

3.3. Связь EditorState ↔ панели

  • Canvas читает app + activePageId + issueNodeIds.
  • PropertiesPanel читает app + selectedNodeId и обновляет только конкретный компонент.
  • StatePanel обновляет app.appState и page.state.
  • EventHandlersPanel обновляет app.eventHandlers и page.watchers.
  • DataSourcesPanel обновляет app.dataSources и их поле mock.
  • AiAssistPanel читает app, projectId, activePage, selectedNode и вызывает callAiAssist, который возвращает обновлённый AppSchema.
  • PreviewPanel читает весь app и activePageId, но не меняет DSL напрямую (все изменения идут только через EditorState).

4. Панели редактора

4.1. Canvas

Canvas визуализирует дерево компонентов DSL (ComponentNode). Основные функции:

  • отображение иерархии rootComponent / layoutComponent для активной страницы;
  • переключение режима Page/Shell: в режиме Shell редактируется app.shell, а в дереве появляется PageOutlet как слот для страницы;
  • выбор компонента (устанавливает selectedNodeId в EditorState);
  • подсветка ошибочных нод (если node.id находится в issueNodeIds);
  • базовые layout‑индикаторы.

В будущем Canvas станет полноценным drag‑and‑drop редактором, но уже сейчас он чётко связан с DSL и системой ошибок.

4.2. PropertiesPanel

PropertiesPanel v2 строится динамически на основе реестра компонентов в @lowcode/dsl:

  • для выбранного ComponentNode берётся его type;

  • через реестр ComponentDefinitionRegistry получаем ComponentDefinition:

    • список пропов,
    • типы (string / number / boolean / any / expression и т.п.),
    • default‑значения,
    • метаданные UI (placeholder, multiline, format…);
  • на основе этого описания формируется форма ввода.

Каждый проп может быть в одном из режимов:

  • StaticValue (string, number, boolean, массивы);
  • ExpressionValue ({ kind: 'expression', expression: string }).

PropertiesPanel:

  • переключает режим (static ↔ expression);
  • приводит строковый ввод к правильному типу (число, boolean);
  • вызывает локальный validateExpressionSyntax для expression‑полей;
  • пишет результат обратно в DSL (в node.props).

Во вкладке Layout & Style используется LayoutStylePanel:

  • редактирует layout и style компонента;
  • секция Interaction states пишет hover‑параметры в styleStates.hover (transformScale, opacity, rotateDeg, transitionMs, transitionEasing, transitionProperty).

Специальная обработка для Image и Video компонентов:

Когда пользователь редактирует src проп компонента Image или Video, PropertiesPanel предоставляет специальный UI для работы с медиа-библиотекой:

  1. MediaLibrary Modal - модальное окно для управления attachments проекта:

    • Просмотр сетки загруженных файлов (browse mode)
    • Детальный просмотр выбранного attachment (details mode)
    • Загрузка новых файлов в S3 или добавление ссылок (upload mode)
    • Поиск по имени файла
    • Фильтрация по MIME type
  2. Фильтрация по типу медиа:

    В PropertiesPanel (Select):

    • Для компонента Image: показываются только изображения
    • Для компонента Video: показываются только видео
    • Фильтрация определяется по MIME type или расширению файла
    • Для backward compatibility показываются файлы с неопределенным типом

    В MediaLibrary Modal:

    • Три таба для фильтрации: All / Images / Videos
    • По умолчанию показываются все файлы (таб "All")
    • Пользователь может переключиться на "Images" или "Videos" для удобства просмотра
    • Ограничение выбора:
      • Если модалка открыта для Image компонента (mimeTypeFilter='image/*'):
        • Можно просматривать все файлы
        • Кнопка "Select" активна только для изображений
        • Для видео кнопка disabled с сообщением "Only images can be selected"
      • Аналогично для Video компонента

    Определение типа файла:

    • Приоритет 1: MIME type из БД (для S3 uploads)
    • Приоритет 2: Расширение файла в downloadUrl query параметре filename= (для Яндекс.Диск)
    • Приоритет 3: Расширение в url или sourceUrl
    • Приоритет 4: Расширение в поле name
  3. UI компонентов:

    • Select с списком совместимых attachments (только изображения для Image, только видео для Video)
    • Кнопка "Edit" для открытия MediaLibrary modal со всеми файлами
    • Превью выбранного файла:
      • Для Image: компонент ImagePreview
      • Для Video: встроенный <video> элемент с controls
  4. Процесс загрузки файла:

    // 1. Запрос presigned URL (5% → 15%)
    const { uploadUrl, storageKey } = await requestUploadUrl({
    projectId,
    filename: file.name,
    contentType: file.type,
    });

    // 2. Загрузка в S3 с отслеживанием прогресса (15% → 85%)
    await uploadFileToS3(uploadUrl, file, file.type, (s3Progress) => {
    // s3Progress: 0-100, масштабируем в диапазон 15-85%
    const scaledProgress = 15 + Math.round((s3Progress / 100) * 70);
    setUploadProgress(scaledProgress);
    });

    // 3. Создание записи attachment (85% → 95%)
    const attachment = await createS3Attachment({
    projectId,
    storageKey,
    name: file.name,
    mimeType: file.type,
    size: file.size,
    });

    // 4. Запись UUID в src проп (95% → 100%)
    onChange(attachment.id);

    Отслеживание прогресса:

    • Функция uploadFileToS3 использует XMLHttpRequest вместо fetch для поддержки событий прогресса
    • xhr.upload.addEventListener('progress') предоставляет реальный прогресс загрузки в S3
    • Прогресс распределяется: 5% (подготовка) → 15% (получение URL) → 85% (загрузка в S3) → 95% (создание записи) → 100% (завершение)
  5. Поддержка двух режимов источника:

    • sourceType='url' - UUID link-attachment (короткая публичная ссылка хранится в sourceUrl)
    • sourceType='upload' - UUID S3 attachment
  6. Ограничения размера файлов:

    • Изображения: максимум 10 МБ
    • Видео: максимум 100 МБ

ВАЖНО:

  • projectId берется из useEditorState() (UUID проекта), а НЕ из app.id (который содержит DSL schema ID вида "app-blank")
  • sourceType проп скрыт от пользователя в UI (автоматически определяется по формату src)
  • MediaLibrary компоненты находятся в apps/builder-web/src/components/MediaLibrary/
  • Для Yandex Disk можно вставлять короткую публичную ссылку — система автоматически получит прямую ссылку на скачивание.
  • В деталях Media Library показываются обе ссылки: public (sourceUrl) и direct (downloadUrl).
  • Для Image.src / Video.src в режиме expression внутри Repeat доступна кнопка Pick attachment:
    • открывает MediaLibrary;
    • подставляет выражение item.imageId (для Image) или item.videoId (для Video);
    • автоматически выставляет sourceType="upload";
    • показывает helper‑текст о необходимости хранить UUID в данных.

4.3. StatePanel

StatePanel работает с двумя уровнями состояния:

  • app.appState.variables — глобальное состояние приложения;
  • page.state.variables — локальное состояние конкретной страницы.

Возможности:

  • создание/удаление переменных;
  • изменение типа (string/number/boolean/any);
  • задание initialValue с учётом типа;
  • флаг required;
  • типобезопасная запись в DSL (корректные JSON‑значения).

Именно отсюда приходят переменные state.*, которые затем доступны в выражениях (ExpressionValue) и попадают в контекст выражений AST.

4.4. EventHandlersPanel

EventHandlersPanel управляет массивом EventHandler[] в DSL:

interface EventHandler {
id: ID;
targetComponentId?: ID;
targetPageId?: ID;
eventName: string;
actions: Action[];
}

Панель позволяет:

  • привязать событие к компоненту (onClick, onChange и т.п.);

  • добавить цепочку Actions:

    • navigate;
    • setState (глобальное/по странице);
    • callDataSource;
    • showToast;
    • log и др.;
  • задавать параметры Actions, в том числе через ExpressionValue.

С точки зрения runtime, эти декларативные Actions превращаются в вызовы actions.* и/или RuntimeCommand, обрабатываемые @lowcode/runtime-core.

В правом сайдбаре есть отдельная вкладка Watchers (панель WatchersPanel) для page.watchers[]:

  • when (ExpressionValue) — выражение, за изменением которого нужно следить;
  • actions[] — что запускать при изменении;
  • mode (change | always), debounceMs, throttleMs. Режим always доступен только при ненулевом debounce или throttle.

Watchers исполняются runtime-core на стороне сгенерированного App.tsx (dsl-compiler), поэтому поведение одинаковое в превью и runtime-host.

При редактировании действий можно использовать event.* внутри выражений. В превью builder-web event имеет нормализованный вид:

{
type: string;
value?: unknown;
target?: {
value?: unknown;
name?: string;
id?: string;
};
originalEvent?: unknown;
}

Для Input.onChange удобно использовать event.value или event.target.value.

4.5. Expression editor (modal)

Во всех местах, где поддерживаются ExpressionValue, доступен режим редактирования через модалку:

  • открывается из ExpressionInput (маленькая кнопка "Edit");
  • редактор построен на CodeMirror 6 и живёт локально в модалке;
  • Save по кнопке или Ctrl/Cmd+S, Undo/Redo — Ctrl/Cmd+Z / Ctrl/Cmd+Y;
  • подсветка синтаксиса, линтер и подсказки автодополнения;
  • Tab и Enter подтверждают автодополнение;
  • показывается список доступных глобальных переменных (state, data, props, event) с контекстными ограничениями (например, event доступен только в хендлерах).

Редактор также использует токены темы (ThemeProvider), поэтому одинаково выглядит в светлой и тёмной теме.

4.5. DataSourcesPanel и моки

DataSourcesPanel работает с app.dataSources и их mock‑конфигурацией.

4.5.1. Структура DataSource

Упрощённо, в DSL есть два типа источников данных:

export type DataSource = StaticDataSource | RestDataSource;

и общее поле mock:

interface DataSourceMockConfig {
enabled: boolean;
activeScenario?: 'success' | 'error';
successMock?: {
mode: 'static' | 'sequence';
value?: StaticValue;
sequence?: StaticValue[];
sequenceValueType?: 'json' | 'text';
delayMs?: number;
};
errorScenarios?: {
api?: {
kind: 'api';
statusCode?: number;
payload?: StaticValue;
delayMs?: number;
};
network?: {
kind: 'network';
message?: string;
statusCode?: number;
statusText?: string;
delayMs?: number;
};
activeKind?: 'api' | 'network';
};
}

DataSourcesPanel позволяет:

  • создавать/удалять источники данных;
  • настраивать REST/STATIC поля источника данных (baseUrl, path, method, headers, queryParams, body, timeoutMs);
  • включать/выключать mock.enabled;
  • настраивать две независимые секции мока: Success response (static / sequence) и Error response (API/Network);
  • выбирать активный сценарий через SegmentedControl (переключатель success/error) и тип ошибочного сценария (errorScenarios.activeKind);
  • задавать value или sequence, тип элементов sequenceValueType, а также задержки для каждого сценария.
  • для JSON‑payload с items[] есть UI‑помощник для выбора attachments и сохранения UUID в items[].imageId (mock success и static data).

4.5.1.1. REST редактор: headers/query/body/timeout

В UI редактора REST‑источника используются компактные пары key/value:

  • Query params (dataSource.config.queryParams) и Headers (dataSource.config.headers) хранятся как Record<string, PropValue>.
  • Поле value вводится через ExpressionInput:
    • если введённый текст можно распарсить как JSON (JSON.parse) — он сохраняется как StaticValue (например, 123, true, "text", { "a": 1 });
    • иначе значение сохраняется как ExpressionValue ({ kind: "expression", expression: "..." }) и вычисляется в runtime.

Таким образом, чтобы задать строковый литерал, нужен валидный JSON (включая кавычки), например "runtime-host". Если кавычки не использовать, строка будет интерпретирована как expression.

Поле Body (dataSource.config.body) также поддерживает ввод expression/JSON (в зависимости от выбранного режима в UI) и подсветку синтаксических ошибок.

Поле Timeout (dataSource.config.timeoutMs) задаётся в миллисекундах и применяется только в runtime-host (в превью — только моки).

Важно: ExpressionValue.expression — это выражение, а не набор операторов; символ ; в конце выражения приведёт к синтаксической ошибке (см. apps/docs/docs/architecture/expression-parser.md).

Поле sequenceValueType фиксирует, как именно пользователь хочет трактовать элементы последовательности: как JSON (json, поведение по умолчанию) или как текст (text, без автопарсинга). Это позволяет избавиться от неоднозначного поведения кнопки Evaluate All.

4.5.2. Поведение моков в превью

В превью builder-web используется default executor из @lowcode/runtime-core:

  • если mock.enabled === true, executor нормализует конфигурацию к successMock и errorScenarios и выбирает сценарий:

    • дефолт — mock.activeScenario (если нет override);
    • RuntimeStateInspector или ручной вызов через callDataSource могут передать mockScenarioOverride, который сохраняется в __mockScenario для devtools;
    • success-сценарий возвращает value либо элементы sequence (циклически, по dataSourceId, с учётом sequenceValueType);
    • error-сценарий выбрасывает ошибку с пользовательским message или дефолтным текстом (а runtime фиксирует её в __lastError).
  • если mock.enabled !== true:

    • для kind="static" возвращается config.data;
    • для kind="rest" и других не‑static‑видов в превью выбрасывается явная ошибка о том, что REST data source без mock не может быть выполнен в превью.

Таким образом:

  • builder-web никогда не делает реальные HTTP‑запросы из превью;
  • все данные для DataSource берутся из mock‑конфигурации или config.data;
  • пользователь может быстро переключаться между success/error сценариями прямо в DataSourcesPanel и в RuntimeStateInspector, мгновенно проверяя оба варианта;
  • если REST-источник не замокан, пользователь получает понятное сообщение о необходимости настроить mock.

4.6. AiAssistPanel

AiAssistPanel — лёгкая панель поверх Canvas/Preview, отвечающая за сценарий:

  • пользователь вводит текстовый запрос (на естественном языке);
  • панель собирает контекст (app, projectId, активную страницу, текущий selection);
  • формируется AiAssistRequest и отправляется на backend (POST /ai/assist);
  • в ответ приходит AiAssistResponse с updatedAppSchema и текстовым explanation;
  • если updatedAppSchema присутствует, он заменяет текущий app в EditorState`.

Основные файлы:

  • apps/builder-web/src/components/AiAssistPanel.tsx — UI и связка с useEditorState + callAiAssist;
  • apps/builder-web/src/api/client.ts — реализация callAiAssist (прокидывает requestId и весь AiAssistRequest на backend);
  • apps/builder-web/src/ai/useAiAssist.ts — (опционально) хук высокого уровня для более сложных сценариев AI‑ассистента.

Важно: UI панели и все пользовательские сообщения — на английском, а архитектурные комментарии и документация — на русском.


5. Валидация DSL и выражений

Валидация в builder-web многоуровневая.

5.1. Структурная валидация DSL (validateAppSchema)

На входе PreviewPanel (и при некоторых действиях редактора) DSL прогоняется через validateAppSchema из @lowcode/dsl.

Проверяется:

  • корректность структуры приложения, страниц, компонент;
  • уникальность id;
  • валидность appState и page.state (типы и значения);
  • базовая корректность DataSources и EventHandlers;
  • корректность типов полей mock.

Ошибка возвращается как список строк, которые в builder-web преобразуются в DslValidationIssue { path, message, severity }.

5.2. Локальная syntax‑валидация выражений (validateExpressionSyntax)

В expressions/validateExpression.ts реализована быстрая проверка синтаксиса для ExpressionValue.expression.

Она используется:

  • при вводе выражения в PropertiesPanel/StatePanel;
  • в PreviewPanel для прохода по DSL (без AST), чтобы собрать первичные diagnostics.

5.3. Семантическая валидация выражений (validateExpressionsInAstApp)

На уровне AST, через @lowcode/dsl-compiler, выполняется более глубокая проверка:

const ast = buildAstFromDsl(app);
const exprDiags = validateExpressionsInAstApp(app, ast);

Здесь выражения уже видят:

  • переменные state.* из глобального/страничного состояния;
  • props.* из ComponentDefinition.props (WIP);
  • data.* из DataSource (включая те, для которых настроены моки), с контрактом data.<source>.value/error/loading/lastResultAt;
  • ожидаемые типы пропов.

Результат — список ExpressionDiagnostic с указанием страницы, компонента и пропа.

5.4. Сведение ошибок в issueNodeIds

PreviewPanel объединяет источники ошибок:

  1. DslValidationIssue[] (структурная валидация DSL);
  2. ExpressionDiagnostic[] (выражения по DSL + AST);
  3. сообщения локального validateExpressionSyntax;
  4. сообщения об ошибках компиляции/исполнения превью (включая ошибки из mock/executor).

Итоговый набор ID передаётся в EditorState через onErrorNodeIdsChange([...ids]) и используется Canvas для подсветки нод с ошибками.


6. Превью и интеграция с runtime-core

6.1. Общий pipeline превью

Текущая версия PreviewPanel (components/PreviewPanel.tsx) работает поверх @lowcode/runtime-core.

Высокоуровневая схема:

graph TD
DSL[AppSchema] --> VALIDATE[validateAppSchema\n+ локальная проверка выражений]
VALIDATE --> AST[buildAstFromDsl\nAstApp]
AST --> EXPR[validateExpressionsInAstApp]
EXPR -->|diagnostics → issueNodeIds| CANVAS[Canvas]
AST --> REACTBNDL[compileDslToReact\n(TSX bundle)]
REACTBNDL --> RUNTIME[@lowcode/runtime-core\ncreateRuntimeInstance]
RUNTIME --> PREVIEW[PreviewPanel\nRuntimeInstance.RootComponent]

Ключевые шаги внутри PreviewPanel:

  1. Сброс состояния при изменении app.
  2. Валидация DSL и выражений.
  3. При ошибках — Canvas подсвечивает ноды, превью не компилируется.
  4. При отсутствии ошибок — компиляция DSL в TSX‑бандл и создание RuntimeInstance.
  5. Передача dataSources и dataSourceExecutor с поддержкой моков.
  6. Формирование обёртки вокруг RootComponent и рендер превью.
  7. Кнопка Reset runtime сбрасывает состояние runtime и перерисовывает превью.
  8. Панель Preview params позволяет задать route.params для активной страницы и применяет их через dispatch({ type: 'navigate', ... }). Query‑параметры (route.query) пока приходят только из runtime‑снапшота и сохраняются при навигации.

6.2. RuntimeStateInspector и лог событий

RuntimeStateInspector и лог событий runtime позволяют наблюдать за:

  • изменениями глобального/страничного состояния;
  • вызовами callDataSource и их результатами (в карточках dataSource видно __lastResultAt, __lastError, __mockScenario);
  • навигацией по страницам;
  • произвольными событиями, помеченными через RuntimeChangeReason.

Inspector также даёт быстрый контроль сценариев моков: SegmentedControl в карточке источника переключает success/error, сохраняет override (mockScenarioOverride) и позволяет вызвать Call data source вручную, не теряя историю state.

Лог событий используется как devtools‑панель под превью.


7. Подсветка ошибок в Canvas

Canvas подсвечивает компоненты, у которых есть ошибки, при помощи issueNodeIds.

Источник issueNodeIds — PreviewPanel, который агрегирует все ошибки из валидации DSL, выражений и runtime и пытается сопоставить их с конкретными componentId.


8. SPA‑архитектура и страницы

Редактор — Single Page Application на React (Vite + React Router).

Типичный набор маршрутов:

/             # стартовая страница / список проектов или шаблонов
/editor # основной режим редактора
/editor/:id # привязка к конкретному проекту (в будущем)

SPA даёт:

  • мгновенные переходы между режимами без перезагрузки страницы;
  • единый EditorState на всём жизненном цикле приложения;
  • возможность держать в DOM одновременно Canvas, Preview, PropertiesPanel, DataSourcesPanel, AiAssistPanel и devtools.

9. Интеграция с AI‑оркестратором

AI‑интеграция в платформе разделена на три слоя:

  1. builder-web — UI и выбор моделей (AiAssistPanel / useAiAssist);
  2. @lowcode/api — HTTP‑эндпоинт /ai/assist и адаптер к @lowcode/ai-orchestrator;
  3. @lowcode/ai-orchestrator — формирование промпта, строгий JSON‑формат ответа и применение операций к DSL.

9.1. Поток запроса AI‑ассистента

Высокоуровнево цепочка выглядит так:

AiAssistPanel (builder-web)
↓ POST /ai/assist
apps/api (AiController/AiService)
↓ assist(input, callModel)
@lowcode/ai-orchestrator
↓ buildAssistModelRequest
↓ AI‑провайдер (OpenAI / AI Tunnel / local)
↓ parseAssistModelResponse
↓ applyAiOperations(appSchema, operations)
→ updatedAppSchema

Builder-web не знает о конкретном HTTP‑эндпоинте AI‑провайдера и API‑ключах — он работает только с абстрактным AiAssistRequest / AiAssistResponse.

9.2. Где задаётся модель по умолчанию

Ключевой момент архитектуры: дефолтные значения AI‑провайдера и модели принадлежат frontend‑слою (builder-web), а не backend‑API.

  • В хуке apps/builder-web/src/ai/useAiAssist.ts заданы значения по умолчанию:

    const DEFAULT_AI_PROVIDER = (import.meta.env && import.meta.env.VITE_AI_PROVIDER) ?? 'direct';
    const DEFAULT_AI_MODEL = (import.meta.env && import.meta.env.VITE_AI_MODEL) ?? 'gpt-4.1-mini';
  • В AiAssistPanel.tsx при формировании запроса в callAiAssist явным образом указываются provider и model. В текущей конфигурации это, как правило, provider: 'aiTunnel' и model: 'gpt-5-mini'.

  • Если в будущем нужно изменить модель по умолчанию, это делается на стороне builder-web:

    • либо через Vite‑переменные окружения VITE_AI_PROVIDER и VITE_AI_MODEL;
    • либо через правку констант/пропсов в useAiAssist.ts или AiAssistPanel.tsx.

Backend apps/api умеет подставлять свои дефолты (через AI_DEFAULT_PROVIDER / AI_DEFAULT_MODEL), но они рассматриваются как страховочная настройка. Архитектурно предполагается, что конкретный provider/model выбирает UI‑слой, и API получает уже полностью определённый AiAssistRequest.

9.3. Формат промпта и ответа (оркестратор)

Весь контракт с моделью описан в @lowcode/ai-orchestrator:

  • packages/ai-orchestrator/src/assist/buildPrompt.ts — формирует:

    • system‑сообщение с детальным описанием роли ассистента и жёстким JSON‑форматом ответа;
    • user‑сообщение с JSON‑payload, содержащим prompt, appSchema, projectId, pageId, hints.
  • packages/ai-orchestrator/src/assist/parseResponse.ts — парсит ответ модели:

    • ожидается чистый JSON‑объект без Markdown;
    • верхний уровень: { "operations": AiOperation[], "explanation": string };
    • массив operations нормализуется к строгому типу AiOperation.
  • packages/ai-orchestrator/src/assist/applyOperations.ts — применяет AiOperation[] к AppSchema и возвращает updatedAppSchema.

Builder-web видит только итоговый AiAssistResponse:

interface AiAssistResponse {
updatedAppSchema?: AppSchema;
operations?: AiOperation[];
explanation?: string;
error?: AiError;
}

и реагирует на него так:

  • если есть error — показывает текст ошибки в панели AI;
  • если есть updatedAppSchema — заменяет текущий app в EditorState;
  • если есть explanation — показывает его в блоке Last explanation.

10. Принципы проектирования builder-web

  1. DSL — единственный источник истины.

    • любой UI‑элемент отражает и редактирует DSL, а не промежуточные структуры;
    • превью и AI‑ассистент всегда работают с актуальным AppSchema.
  2. Иммутабельность и прозрачные обновления.

    • все изменения делаются через pure‑функции обновления DSL;
    • EditorState остаётся предсказуемым, возможен undo/redo.
  3. Разделение слоёв: DSL → AST → runtime → AI.

    • DSL‑валидация и AST‑анализ живут в @lowcode/dsl и @lowcode/dsl-compiler;
    • исполнение бандла и состояние — в @lowcode/runtime-core;
    • генерация операций AI‑ассистентом — в @lowcode/ai-orchestrator;
    • builder-web отвечает за UX и выбор моделей.
  4. Ошибки должны быть видимы.

    • любая ошибка (DSL, выражения, runtime, моки, AI) подсвечивается в UI;
    • Canvas, панели и AiAssistPanel позволяют быстро найти и исправить источник.
  5. ExpressionValue — полноценный гражданин DSL.

    • выражения валидируются и по синтаксису, и по семантике;
    • в будущем — подсветка и интерактивная отладка выражений.
  6. UI-kit как единая дизайн‑система.

    • LayoutShell, панели, карточки, кнопки и формы построены на @lowcode/ui-kit;
    • runtime-host будет использовать тот же UI‑kit.
  7. Моки — обязательный слой между превью и внешним миром.

    • builder-web не делает боевые запросы к API;
    • превью опирается только на mock‑данные или статический config.data;
    • runtime-host отвечает за реальные HTTP/DB/WebSocket‑вызовы.
  8. AI‑ассистент — дополнение к ручному редактированию, а не замена.

    • AI генерирует декларативные операции поверх существующего DSL;
    • пользователь всегда может донастроить результат через панели;
    • формат операций полностью контролируется платформой.
  9. Расширяемость и эволюция.

    • любые новые функции проходят тот же путь: DSL → AST → Preview → Runtime → AI;
    • архитектура допускает появление новых компонентов, действий, dataSources, mock‑режимов, выражений и AI‑операций без ломки ядра.

Builder-web уже сейчас выступает как центральный интерфейс Lowcode‑платформы и основное рабочее место для создания и сопровождения DSL‑приложений — как вручную, так и с помощью AI‑ассистента.