diff --git a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx index 1a150242..ce853435 100644 --- a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx +++ b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx @@ -6,7 +6,7 @@ import { useRef, useState, } from "react"; -import { Group, Loader, Select, Tooltip } from "@mantine/core"; +import { Group, Loader, Tooltip } from "@mantine/core"; import { IconArrowsDiagonal, IconCheck, @@ -37,6 +37,7 @@ import { } from "@/features/ai-chat/queries/ai-chat-query.ts"; import ConversationList from "@/features/ai-chat/components/conversation-list.tsx"; import ChatThread from "@/features/ai-chat/components/chat-thread.tsx"; +import RoleCards from "@/features/ai-chat/components/role-cards.tsx"; import { buildChatMarkdown } from "@/features/ai-chat/utils/chat-markdown.ts"; import { shouldCollapseOnOutsidePointer, @@ -145,6 +146,19 @@ export default function AiChatWindow() { () => (roles ?? []).filter((r) => r.enabled === true), [roles], ); + + // Role cards become the empty-state ONLY for a brand-new chat that has roles. + // Once the chat has messages, MessageList no longer renders the empty-state, + // so `selectedRoleId` staying null naturally falls back to Universal assistant + // (no "reset on send" logic needed). + const roleCardsNode = + activeChatId === null && enabledRoles.length > 0 ? ( + + ) : undefined; const { data: messageRows, isLoading: messagesLoading } = useAiChatMessagesQuery(activeChatId ?? undefined); @@ -537,28 +551,10 @@ export default function AiChatWindow() { )} - {/* Role picker — only for a NEW chat (before it is created). Once the - chat exists, its role is fixed and shown as a header badge instead. - Defaults to "Universal assistant" (no role). */} - {activeChatId === null && (enabledRoles?.length ?? 0) > 0 && ( -
- `. Просьба: заменить список на **карточки -разных цветов с названием identity по центру пустого окна чата**. Клик по -карточке применяет роль; если пользователь карточку не нажал и просто написал -сообщение — срабатывает дефолтный Universal assistant. - -Скриншот текущего поведения приложил пользователь: «Agent role» + раскрытый -список (Universal assistant ✓, Пират, Дедушка). - -## Как сейчас устроен выбор роли (цепочка) - -1. Picker рисуется только для нового чата (`activeChatId === null`), когда есть - включённые роли, как `` — карточки разных цветов по центру пустого окна чата. -- Каждая карточка = identity (роль), отдельный цвет, по центру эмодзи + имя. -- Отдельная карточка **Universal assistant** (дефолт), подсвечена по умолчанию. -- Клик по карточке выбирает/применяет identity (визуальная подсветка выбранной). -- Если ни одна карточка не нажата и пользователь отправил сообщение → роль `null` - → Universal assistant (текущая дефолтная ветка сервера). -- После отправки первого сообщения карточки исчезают (чат больше не пуст). - -## Ключевое архитектурное решение - -Рисовать карточки **как empty-state** окна чата через уже существующий проп -`emptyState` у `MessageList`, а НЕ отдельным блоком над полем ввода. Почему так: - -- «посреди пустого окна чата» получается само: `MessageList` оборачивает - `emptyState` в `
` (`message-list.tsx:130-140`). -- «не нажал и написал сообщение → дефолт» получается само: как только - `messages.length > 0`, empty-state (и карточки) не рендерится, а - `selectedRoleId` остаётся `null` → Universal assistant. Никакой логики - «сбросить выбор при отправке» не нужно. -- Состояние выбора остаётся в том же `selectedAiRoleIdAtom`, поэтому вся - серверная обвязка (`roleId` в body, фиксация роли при создании чата) **не - меняется** — изменения чисто фронтовые. - -Поток: `AiChatWindow` собирает узел карточек → новый проп `emptyState` у -`ChatThread` → форвард в `MessageList`. - -## Состав изменений - -1. **Новый компонент `role-cards.tsx`** (+ `role-cards.module.css`), - `apps/client/src/features/ai-chat/components/`: - - Пропсы: `roles: IAiRole[]`, `selectedRoleId: string | null`, - `onSelect: (id: string | null) => void`. - - Рендер: контейнер карточек с переносом (flex-wrap), по центру: - - первая карточка — Universal assistant (значение `null`), нейтрально-серая, - подсвечена когда `selectedRoleId === null`; - - по карточке на каждую роль: цвет по индексу, по центру эмодзи (если есть) - + имя; подсвечена когда `selectedRoleId === r.id`. - - Карточка — `UnstyledButton` (доступность + темизация Mantine). Клик → - `onSelect(value)`. Выбранная — более яркий бордер/кольцо + галочка. - - Цвета — фиксированная палитра имён Mantine, циклично по индексу: - `blue, grape, teal, orange, pink, cyan, lime, indigo, red, violet`. - Через theme-aware CSS-переменные (корректны и в светлой, и в тёмной теме): - фон `var(--mantine-color-${c}-light)`, текст - `var(--mantine-color-${c}-light-color)`, бордер выбранной - `var(--mantine-color-${c}-filled)`. Universal — `gray`. - - Раскладка (размер карточек ~100–130px, отступы, hover, кольцо выбора, - прокрутка при большом числе ролей) — в CSS-модуле; цвет инжектится инлайн. - -2. **`ai-chat-window.tsx`**: - - Удалить блок ``: - `r.emoji ? ... : ''`). - -## Локализация - -Новых ключей не требуется: переиспользуем существующие `t("Agent role")` и -`t("Universal assistant")` (есть в `apps/client/public/locales/en-US/translation.json:1220-1221`; -остальные локали падают на ключ — как сейчас у `