BR 6.3 — Admin Web

Floating chat-bubble + чат-окно AI-агента. Доступно на всех страницах админки франшизы.

Файлы для создания / редактирования

Создать

web/src/
├── components/agent/
│   ├── AgentChatBubble.tsx       ← Floating button (FAB), main wrapper
│   ├── AgentChatWindow.tsx       ← Окно 380×600
│   ├── AgentMessageList.tsx      ← Лента сообщений
│   ├── AgentMessage.tsx          ← Одно сообщение (user / assistant / tool inline)
│   ├── AgentInput.tsx            ← Textarea + send button
│   ├── AgentToolBlock.tsx        ← inline-блок «🔍 ищу...» / «✓ нашёл»
│   └── AgentEmpty.tsx            ← Приветствие + примеры команд
├── api/
│   └── agent.ts                  ← API-клиент (chat SSE, conversations CRUD)
└── hooks/
    ├── useAgentChat.ts           ← state machine: idle / sending / streaming / error
    └── useAgentSSE.ts            ← Подписка на SSE через fetch+reader

Редактировать

  • web/src/components/layout/Layout.tsx — добавить <AgentChatBubble /> рядом с любым <Outlet />, только если permissions.includes('agent.use')
  • web/src/lib/permissions.ts (если такой есть) — добавить agent.use, agent.config в enum

Поведенческая спека

Полное описание UI / состояний / поведения — AI-агент — Чат.

Ключевые моменты:

  • Стрим через fetch с reader (не EventSource — нужны headers)
  • Парсер SSE-блоков event: <type>\ndata: <json>\n\n
  • Сохранение conversation_id в localStorage (agent_current_conversation)
  • Перерисовка tool-блоков inline с anim spinner

API-клиент (api/agent.ts)

import { authFetch } from "./client";
 
export interface StreamEvent {
  type: 'conversation' | 'message_started' | 'thinking' | 'tool_call_started' | 'tool_call_completed' | 'content_delta' | 'message_completed' | 'error' | 'done';
  // ... payload-specific
}
 
export async function* streamChat(
  body: { conversation_id?: string; message: string },
  signal: AbortSignal,
): AsyncIterableIterator<StreamEvent> {
  const res = await authFetch('/api/v1/admin/agent/chat', {
    method: 'POST',
    body: JSON.stringify(body),
    signal,
    headers: { 'Accept': 'text/event-stream' },
  });
  if (!res.ok || !res.body) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err?.error?.message ?? `HTTP ${res.status}`);
  }
  
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) return;
    buffer += decoder.decode(value, { stream: true });
    
    // Парсим блоки event:...\ndata:...\n\n
    while (true) {
      const idx = buffer.indexOf('\n\n');
      if (idx === -1) break;
      const block = buffer.slice(0, idx);
      buffer = buffer.slice(idx + 2);
      const eventLine = block.match(/^event: (.+)$/m)?.[1];
      const dataLine = block.match(/^data: (.+)$/m)?.[1];
      if (!eventLine || !dataLine) continue;
      yield { type: eventLine as any, ...JSON.parse(dataLine) };
    }
  }
}
 
export async function listConversations(): Promise<Conversation[]> { /* GET /api/v1/admin/agent/conversations */ }
export async function getConversation(id: string): Promise<ConversationWithMessages> { /* */ }
export async function deleteConversation(id: string): Promise<void> { /* DELETE */ }

Permission-guard

В Layout.tsx:

import AgentChatBubble from '../agent/AgentChatBubble';
// ...
{permissions.includes('agent.use') && <AgentChatBubble />}

Стилизация

Использовать существующие Alfa-tokens (web/src/lib/tokens.ts):

  • FAB кнопка: background: colors.red, color: colors.bg
  • Окно чата: background: colors.bg, border: 1px solid colors.border, shadow: shadow.md
  • User-сообщения: background: colors.bg, border: 1px solid colors.border
  • Assistant-сообщения: без бордера, основной текст
  • Tool-блоки: color: colors.textMuted, font-size: 12px
  • Spinner: colors.red

После реализации можно прогнать /alfa-restyle components/agent для финальной полировки.

Тесты

  • Vitest + React Testing Library:
    • Опционально (если в репо есть тесты)
    • Mock SSE-stream через MockReadableStream
    • Проверить что tool-блоки появляются inline и обновляются

Готовность

  • Компонент AgentChatBubble рендерится на всех страницах если есть permission
  • Открытие окна показывает либо empty state, либо последнюю сессию из localStorage
  • Отправка сообщения стримит блоки thinking/tool/content
  • Закрытие окна не теряет conversation_id (вернёшься — продолжается)
  • 503 от API → визуальная ошибка «AI временно недоступен»
  • 429 → toast «Слишком много запросов»
  • TypeScript strict — нет ошибок
  • Билд проходит (pnpm --filter @erp/admin-web build)

Ссылки