Чат ИИ-агента: закрепление в боковом меню (dock) #276

Closed
opened 2026-07-02 00:48:00 +03:00 by vvzvlad · 0 comments
Owner

Задача

Дать возможность перетащить плавающее окно чата ИИ‑агента на боковое меню (навбар с деревом страниц). При отпускании над меню окно закрепляется, занимая область навбара и перекрывая дерево. При закрытии чата дерево снова показывается. Открепление — кнопкой в шапке окна и вытягиванием мышью обратно на контент.

Поведение (acceptance criteria)

  • Перетаскивание плавающего окна на область бокового меню и отпускание над ним → окно закрепляется, перекрывая дерево страниц.
  • Во время перетаскивания над меню показывается подсветка drop‑зоны (габариты навбара).
  • Закрытие чата (крестик) в закреплённом режиме → дерево снова видно и доступно.
  • Открепление кнопкой dock/undock в шапке окна.
  • Открепление вытягиванием: перетащить закреплённое окно из области меню на контент → окно всплывает у точки броса.
  • Режим (закреплён/плавающий) сохраняется между перезагрузками и закрытием/открытием (localStorage).
  • Закреплённое окно корректно следует за шириной навбара (ручной ресайз, смена маршрута пространство ↔ общий).
  • Стрим ответа агента не обрывается при dock/undock.

Как устроено сейчас

  • Окно чата — единственный, глобально смонтированный компонент apps/client/src/features/ai-chat/components/ai-chat-window.tsx, рендерится один раз в global-app-shell.tsx. Оно position: fixed, геометрия хранится в aiChatWindowGeomAtom (localStorage), z-index = 105.
  • Внутри висит ChatThread со стримом (useChat + AbortController). Тело не размонтируется даже при сворачивании → закрепление нельзя делать переносом компонента внутрь навбара (это вызовет remount и обрыв стрима). Режим dock обязан быть переключением CSS/геометрии на том же инстансе.
  • Дерево живёт в AppShell.Navbar. Z-index навбара = 101 (Mantine база 100 +1), шапки = 100 → окно (105) корректно перекроет дерево и не заденет шапку.
  • Закрытие уже прячет окно, а дерево всегда отрисовано под ним → «закрыл чат → видно дерево» получается автоматически.

План реализации

Добавить персистентный флаг docked. Тот же инстанс окна в режиме dock переключается со стиля «плавающее по geom» на «прибитое к живому getBoundingClientRect() навбара» (по стабильному id), с синхронизацией через ResizeObserver + resize.

Файлы:

  1. ai-chat/atoms/ai-chat-atom.ts — новый aiChatWindowDockedAtom (atomWithStorage<boolean>, default false).
  2. layouts/global/hooks/atoms/sidebar-atom.ts — экспорт константы APP_NAVBAR_ID = "app-shell-navbar" (кладём сюда во избежание import‑цикла shell ↔ chat window).
  3. layouts/global/global-app-shell.tsx — повесить id={APP_NAVBAR_ID} на <AppShell.Navbar>.
  4. ai-chat/components/ai-chat-window.tsx — основная логика:
    • состояние docked / dockRect / dockHint, refs dockedRef / geomRef;
    • хелперы getNavbarRect() / isPointerOverNavbar();
    • useLayoutEffect синхронизации dockRect (before paint, ResizeObserver на навбаре + window resize, повтор по location.pathname);
    • ветвление inline‑стиля и классов (.docked), fallback на плавающий вид если навбар недоступен;
    • startDrag: решение dock/undock на mouseup (над навбаром → dock; для закреплённого — отпускание вне навбара → undock у точки броса через clampGeom);
    • кнопка dock/undock в шапке (IconLayoutSidebarLeftCollapse / IconLayoutSidebarLeftExpand), скрытие «Свернуть» в режиме dock;
    • подсветка drop‑зоны при dockHint;
    • в режиме dock отключить автосворачивание при клике вне окна и ResizeObserver‑персист размеров.
  5. ai-chat/components/ai-chat-window.module.css — классы .docked (без border-radius/тени/resize, снятые min/max) и .dockHighlight (z-index 106, dashed border, полупрозрачная заливка).
  6. public/locales/en-US и ru-RU — ключи "Dock to sidebar" / "Undock".

Крайние случаи

  • Навбар свёрнут/отсутствует → getNavbarRect() возвращает null, окно откатывается в плавающий вид (не «исчезает»).
  • Смена маршрута (пространство ↔ общий) меняет ширину навбара — ловится ResizeObserver + повторный запуск эффекта по location.pathname.
  • min-width плавающего окна (300px) больше минимума навбара (220px) → в .docked снимаем min/max, размеры целиком из inline‑стиля от dockRect.
  • z-index 105 < 200/300/9999 — поиск/меню/модалки/уведомления остаются поверх закреплённого окна.

Вне рамок

Логика стрима/сессии (useChatSession, ChatThread, адаптация id чата), conversation-list, серверная часть — не трогаем. Только фронт.

## Задача Дать возможность перетащить плавающее окно чата ИИ‑агента на боковое меню (навбар с деревом страниц). При отпускании над меню окно **закрепляется**, занимая область навбара и перекрывая дерево. При закрытии чата дерево снова показывается. Открепление — кнопкой в шапке окна **и** вытягиванием мышью обратно на контент. ## Поведение (acceptance criteria) - [ ] Перетаскивание плавающего окна на область бокового меню и отпускание над ним → окно закрепляется, перекрывая дерево страниц. - [ ] Во время перетаскивания над меню показывается подсветка drop‑зоны (габариты навбара). - [ ] Закрытие чата (крестик) в закреплённом режиме → дерево снова видно и доступно. - [ ] Открепление кнопкой dock/undock в шапке окна. - [ ] Открепление вытягиванием: перетащить закреплённое окно из области меню на контент → окно всплывает у точки броса. - [ ] Режим (закреплён/плавающий) сохраняется между перезагрузками и закрытием/открытием (localStorage). - [ ] Закреплённое окно корректно следует за шириной навбара (ручной ресайз, смена маршрута пространство ↔ общий). - [ ] Стрим ответа агента не обрывается при dock/undock. ## Как устроено сейчас - Окно чата — единственный, глобально смонтированный компонент `apps/client/src/features/ai-chat/components/ai-chat-window.tsx`, рендерится один раз в `global-app-shell.tsx`. Оно `position: fixed`, геометрия хранится в `aiChatWindowGeomAtom` (localStorage), z-index = 105. - Внутри висит `ChatThread` со стримом (`useChat` + `AbortController`). Тело не размонтируется даже при сворачивании → **закрепление нельзя делать переносом компонента внутрь навбара** (это вызовет remount и обрыв стрима). Режим dock обязан быть переключением CSS/геометрии на том же инстансе. - Дерево живёт в `AppShell.Navbar`. Z-index навбара = 101 (Mantine база 100 +1), шапки = 100 → окно (105) корректно перекроет дерево и не заденет шапку. - Закрытие уже прячет окно, а дерево всегда отрисовано под ним → «закрыл чат → видно дерево» получается автоматически. ## План реализации Добавить персистентный флаг `docked`. Тот же инстанс окна в режиме dock переключается со стиля «плавающее по geom» на «прибитое к живому `getBoundingClientRect()` навбара» (по стабильному `id`), с синхронизацией через `ResizeObserver` + `resize`. Файлы: 1. **`ai-chat/atoms/ai-chat-atom.ts`** — новый `aiChatWindowDockedAtom` (`atomWithStorage<boolean>`, default `false`). 2. **`layouts/global/hooks/atoms/sidebar-atom.ts`** — экспорт константы `APP_NAVBAR_ID = "app-shell-navbar"` (кладём сюда во избежание import‑цикла shell ↔ chat window). 3. **`layouts/global/global-app-shell.tsx`** — повесить `id={APP_NAVBAR_ID}` на `<AppShell.Navbar>`. 4. **`ai-chat/components/ai-chat-window.tsx`** — основная логика: - состояние `docked` / `dockRect` / `dockHint`, refs `dockedRef` / `geomRef`; - хелперы `getNavbarRect()` / `isPointerOverNavbar()`; - `useLayoutEffect` синхронизации `dockRect` (before paint, `ResizeObserver` на навбаре + `window resize`, повтор по `location.pathname`); - ветвление inline‑стиля и классов (`.docked`), fallback на плавающий вид если навбар недоступен; - `startDrag`: решение dock/undock на `mouseup` (над навбаром → dock; для закреплённого — отпускание вне навбара → undock у точки броса через `clampGeom`); - кнопка dock/undock в шапке (`IconLayoutSidebarLeftCollapse` / `IconLayoutSidebarLeftExpand`), скрытие «Свернуть» в режиме dock; - подсветка drop‑зоны при `dockHint`; - в режиме dock отключить автосворачивание при клике вне окна и `ResizeObserver`‑персист размеров. 5. **`ai-chat/components/ai-chat-window.module.css`** — классы `.docked` (без border-radius/тени/resize, снятые min/max) и `.dockHighlight` (z-index 106, dashed border, полупрозрачная заливка). 6. **`public/locales/en-US` и `ru-RU`** — ключи `"Dock to sidebar"` / `"Undock"`. ## Крайние случаи - Навбар свёрнут/отсутствует → `getNavbarRect()` возвращает `null`, окно откатывается в плавающий вид (не «исчезает»). - Смена маршрута (пространство ↔ общий) меняет ширину навбара — ловится `ResizeObserver` + повторный запуск эффекта по `location.pathname`. - min-width плавающего окна (300px) больше минимума навбара (220px) → в `.docked` снимаем `min/max`, размеры целиком из inline‑стиля от `dockRect`. - z-index 105 < 200/300/9999 — поиск/меню/модалки/уведомления остаются поверх закреплённого окна. ## Вне рамок Логика стрима/сессии (`useChatSession`, `ChatThread`, адаптация id чата), `conversation-list`, серверная часть — не трогаем. Только фронт.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#276