AI-агент — Чат (floating)

Роуты: нет своего — chat-bubble накладывается поверх любой страницы админки. API: POST /api/v1/admin/agent/chat (SSE), GET /api/v1/admin/agent/conversations, DELETE /api/v1/admin/agent/conversations/{id}

Статус реализации

  • Backend: не начатerp-openclaw-agent ещё не создан как репозиторий (самопис erp-llm-gateway отказан 2026-05-13, агент ходит к LLM-провайдеру напрямую — TBD)
  • Admin BFF: не начат — нет proxy-роута /api/v1/admin/agent/*
  • Admin Web: не начат — нет компонента AgentChatBubble
  • Permission agent.use ещё не в каталоге User Service (миграция требуется)

Источники


Размещение в UI

Floating button в правом нижнем углу страницы (position: fixed; bottom: 24px; right: 24px; z-index: 1000):

  • Иконка: (sparkle, как в большинстве AI-инструментов)
  • Размер: 56×56 px, круглая, красный (colors.red), shadow shadow.md
  • Hover: scale 1.05, colors.redHover
  • Tooltip: «AI-помощник (бета)»
  • Permission-guard: скрыта если у юзера нет agent.use (через UI-гейтинг)

При клике — открывается чат-окно.


Чат-окно

Положение: anchored к floating button, поднимается вверх. Десктоп: 380×600 px, fixed bottom-right.

Структура:

┌────────────────────────────────────────┐
│ AI-помощник                  [⋯] [✕]   │  ← header
├────────────────────────────────────────┤
│                                        │
│  Привет! Я могу:                       │
│    🔍 искать товары                    │
│    📝 создавать заказы                 │
│    🚫 управлять стоп-листом            │
│    📊 показывать сводку продаж         │
│                                        │
│  Что нужно сделать?                    │
│                                        │
│              ┌─────────────────────┐   │
│              │ Создай заказ на     │   │  ← user message
│              │ маргариту и колу    │   │
│              │ на вынос для Арбата │   │
│              └─────────────────────┘   │
│                                        │
│  🔍 ищу товары "маргарита"             │  ← tool_call_started
│  ✓ нашёл: Пицца Маргарита 590 ₽        │  ← tool_call_completed
│  🔍 ищу товары "кола"                  │
│  ✓ нашёл: Кока-кола 260 ₽              │
│  📝 создаю заказ...                    │
│  ✓ заказ #1234, сумма 850 ₽            │
│                                        │
│  Заказ #1234 создан: Пицца Маргарита,  │  ← assistant final content
│  Кока-кола. Сумма 850 ₽.               │
│                                        │
├────────────────────────────────────────┤
│ ┌──────────────────────────────────┐   │
│ │ Введи команду…              [→]  │   │  ← input
│ └──────────────────────────────────┘   │
└────────────────────────────────────────┘

Header:

  • Заголовок «AI-помощник» (h3)
  • Бэйдж «бета» рядом
  • Кнопка «⋯» (меню): «История сессий», «Очистить чат», «Начать новый»
  • Кнопка «✕» — закрыть окно (chat-bubble остаётся на странице)

Сообщения (history):

  • User: справа, белый бэкграунд, border 1px colors.border, padding 12px, border-radius 12px (с уголком снизу-справа)
  • Assistant final content: слева, без бордера, основной текст (type.body)
  • Tool call started / completed: inline-блоки между assistant content. Иконка слева (🔍 / ✓ / ✗), текст colors.textMuted, размер 12px (type.small). Анимация спиннера на active

Input:

  • Textarea, авторазмер по содержимому (1-4 строки)
  • Кнопка отправки справа (стрелка)
  • Enter → отправить, Shift+Enter → перенос строки
  • Лимит 2000 символов (счётчик появляется при ≥ 1800)

Состояния

СостояниеЧто видит юзер
Empty (новая сессия)Приветствие + 4 примера команд (по одному tool каждой)
SendingInput заблокирован, плейсхолдер «Отправляю…»
StreamingВидны input заблокирован, пузырь ассистента наполняется, tool-блоки появляются по мере выполнения
Idle (есть история)Скролл к низу, новый input готов
Conversation limit (20 ходов)Input блокирован, баннер «В этой сессии 20 ходов — начни новый чат» с кнопкой «Новый чат»
Rate limit (429)Toast «Слишком много запросов, подожди 38 сек»
LLM offline (503)Внутри чата плейсхолдер: «AI временно недоступен. Алексей в курсе.» + кнопка «Повторить»
No permission (agent.use нет)Floating button скрыт

Поток (state machine)

stateDiagram-v2
    [*] --> ButtonHidden : нет agent.use
    [*] --> ButtonClosed : есть agent.use
    
    ButtonClosed --> ChatOpen : клик
    ChatOpen --> ChatOpen_Idle : OK (есть/нет история)
    ChatOpen --> ChatOpen_Closed : клик ✕
    ChatOpen_Closed --> ButtonClosed
    
    ChatOpen_Idle --> Sending : submit message
    Sending --> Streaming : 200 + SSE 'conversation' event
    Sending --> Error_LLMOff : 503
    Sending --> Error_Rate : 429
    Sending --> Error_Limit : 409 conversation limit
    
    Streaming --> Streaming : SSE chunk (delta / tool_call / etc.)
    Streaming --> ChatOpen_Idle : SSE 'done'
    Streaming --> Error_Stream : SSE 'error' event
    
    Error_LLMOff --> ChatOpen_Idle : клик "Повторить"
    Error_Rate --> ChatOpen_Idle : через 38 сек
    Error_Stream --> ChatOpen_Idle : визит на UI, оставляем чат
    Error_Limit --> NewConversation : клик "Новый чат"
    NewConversation --> ChatOpen_Idle

История сессий (опционально)

Через меню «⋯ → История» → выпадающий список последних 10 сессий:

История
─────────────────────
• Создай заказ на… (5 мин)
• Что в стоп-листе? (1 ч)
• Сколько продали… (вчера)
• ...

Клик → загружается история в текущее окно (GET /conversations/{id}).

API: GET /api/v1/admin/agent/conversations?limit=10.

В MVP — можно сделать только «Новый чат» / текущая сессия, без сайдбара истории (упрощение).


Технические детали

Хранение текущей сессии:

  • conversation_id в localStorage ключе agent_current_conversation — переживает F5
  • При закрытии вкладки сессия остаётся active в БД — можно вернуться

SSE-клиент:

  • Использовать EventSource или fetch с reader (для headers — нужен fetch, не EventSource)
  • Парсить каждый event: + data: блок
  • На done → закрыть стрим, разблокировать input
  • При обрыве сети → toast «Связь прервалась, отправь ещё раз»

BFF-роут:

  • POST /api/v1/admin/agent/chat в admin-bff проксирует напрямую в erp-openclaw-agent с accept: text/event-stream
  • Стрим пробрасывается без преобразований

Permission-guard:

  • В Layout.tsx (Admin Web) рендер <AgentChatBubble /> только если permissions.includes('agent.use')
  • См. UI-гейтинг

Что НЕ делаем в MVP

  • ❌ Голосовой ввод
  • ❌ Превью результатов tool с картинками / графиками (пока только текст-summary)
  • ❌ Возможность отменить активный tool_call (cancel)
  • ❌ Markdown-рендеринг в ответах (только текст)
  • ❌ Подсказки / autocomplete команд
  • ❌ Drag-resize окна чата

Связи