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)