feat(ai-chat): показывать reasoning live во время фазы «думания» (до начала ответа) #178

Closed
opened 2026-06-24 22:49:23 +03:00 by vvzvlad · 0 comments
Owner

Проблема

Reasoning («ход мысли» модели) уже стримится с сервера и рендерится клиентом, но текст размышления становится виден только после того, как начался ответный текст. Во время чистой фазы «думания» (а для reasoning-моделей вроде GLM/o-серии она бывает долгой) пользователь видит только индикатор Thinking… · N tokens — сам ход мысли скрыт, пока модель не начнёт отвечать.

Почему так сейчас

assistantMessageHasVisibleContent не считает reasoning-парт «видимым контентом» (видимы только непустой text, tool-парт, persisted error/aborted):

А MessageItem при отсутствии видимого контента возвращает null, отдавая фазу «думания» отдельному TypingIndicator:

Серверный форвардинг reasoning уже включён (sendReasoning: true, ai-chat.service.ts:594; публичный share — на дефолте v6), так что дельты приходят на клиент в реальном времени — их просто не показывают как текст до начала ответа.

Желаемое поведение

Показывать ReasoningBlock (раскрытый и наполняемый live) сразу, как только появился непустой reasoning-парт — ещё до прихода ответного текста. По завершении turn'а блок можно автоматически сворачивать.

Набросок реализации (точечные правки)

  • [message-content.ts] assistantMessageHasVisibleContent: считать сообщение видимым при наличии непустого reasoning-парта. Это единый источник истины, синхронизированный с typingIndicatorShowsName (см. комментарии в файле) — менять аккуратно, чтобы во время фазы «думания» имя агента и лейаут держал ровно один элемент и не было прыжка/дубля.
  • [message-item.tsx] reasoning уже рендерится (стр. 95-103); снять/смягчить ранний return null (стр. 80) для случая «только reasoning».
  • [typing-indicator.tsx] согласовать: когда reasoning-блок уже виден в бабле, отдельный Thinking…-индикатор не должен дублировать имя/строку (есть проп showName и логика typingIndicatorShowsName). Возможно, во время чистого reasoning индикатор «дотов» оставить внутри/рядом с блоком, а имя отдать баблу.
  • [reasoning-block.tsx] опционально: авто-раскрытие во время стрима, авто-сворачивание после finish.

Связанные изменения

  • #175 (surfacing reasoning) и PR #177 / селектор «тип протокола» — именно они начинают доставлять reasoning_content, ради которого эта UX-доработка имеет смысл.
  • Для авторитетного (а не оценочного) счётчика reasoning-токенов на finish-step нужен includeUsage: true у createOpenAICompatible (см. ревью PR #177) — живой счётчик из стрима работает и без него.

Критерии приёмки

  • При долгой фазе размышления (текста ответа ещё нет) виден раскрытый ReasoningBlock, наполняющийся в реальном времени.
  • Имя агента и лейаут не «прыгают» при переходе reasoning → ответный текст (нет дубля имени из индикатора и бабла).
  • После завершения turn'а reasoning остаётся доступен (сворачиваемый блок), переживает перезагрузку истории и попадает в markdown-экспорт — как сейчас.
  • Поведение консистентно на аутентифицированном чате и публичном share.
  • Сообщения, где reasoning пустой/только счётчик, не создают пустой 0-токенный блок (текущая защита сохранена).

Заметка

Стейл-комментарий: message-item.tsx:41 гласит «reasoning … ignored for v1», хотя код ниже его рендерит — заодно поправить.

🤖 Generated with Claude Code

## Проблема Reasoning («ход мысли» модели) уже стримится с сервера и рендерится клиентом, но **текст размышления становится виден только после того, как начался ответный текст**. Во время чистой фазы «думания» (а для reasoning-моделей вроде GLM/o-серии она бывает долгой) пользователь видит только индикатор `Thinking… · N tokens` — сам ход мысли скрыт, пока модель не начнёт отвечать. ## Почему так сейчас `assistantMessageHasVisibleContent` **не считает reasoning-парт «видимым контентом»** (видимы только непустой text, tool-парт, persisted error/aborted): - [apps/client/src/features/ai-chat/utils/message-content.ts:34-38](apps/client/src/features/ai-chat/utils/message-content.ts#L34-L38) А `MessageItem` при отсутствии видимого контента возвращает `null`, отдавая фазу «думания» отдельному `TypingIndicator`: - гейт: [apps/client/src/features/ai-chat/components/message-item.tsx:80](apps/client/src/features/ai-chat/components/message-item.tsx#L80) - сам рендер reasoning (уже есть!) — `<ReasoningBlock>`: [apps/client/src/features/ai-chat/components/message-item.tsx:95-103](apps/client/src/features/ai-chat/components/message-item.tsx#L95-L103) - индикатор с живым счётчиком токенов: [apps/client/src/features/ai-chat/components/typing-indicator.tsx:43-46](apps/client/src/features/ai-chat/components/typing-indicator.tsx#L43-L46) Серверный форвардинг reasoning уже включён (`sendReasoning: true`, [ai-chat.service.ts:594](apps/server/src/core/ai-chat/ai-chat.service.ts#L594); публичный share — на дефолте v6), так что дельты приходят на клиент в реальном времени — их просто не показывают как текст до начала ответа. ## Желаемое поведение Показывать `ReasoningBlock` (раскрытый и наполняемый **live**) сразу, как только появился непустой reasoning-парт — ещё до прихода ответного текста. По завершении turn'а блок можно автоматически сворачивать. ## Набросок реализации (точечные правки) - **[message-content.ts]** `assistantMessageHasVisibleContent`: считать сообщение видимым при наличии непустого reasoning-парта. Это **единый источник истины**, синхронизированный с `typingIndicatorShowsName` (см. комментарии в файле) — менять аккуратно, чтобы во время фазы «думания» имя агента и лейаут держал ровно один элемент и не было прыжка/дубля. - **[message-item.tsx]** reasoning уже рендерится (стр. 95-103); снять/смягчить ранний `return null` (стр. 80) для случая «только reasoning». - **[typing-indicator.tsx]** согласовать: когда reasoning-блок уже виден в бабле, отдельный `Thinking…`-индикатор не должен дублировать имя/строку (есть проп `showName` и логика `typingIndicatorShowsName`). Возможно, во время чистого reasoning индикатор «дотов» оставить внутри/рядом с блоком, а имя отдать баблу. - **[reasoning-block.tsx]** опционально: авто-раскрытие во время стрима, авто-сворачивание после finish. ## Связанные изменения - #175 (surfacing reasoning) и PR #177 / селектор «тип протокола» — именно они начинают доставлять `reasoning_content`, ради которого эта UX-доработка имеет смысл. - Для **авторитетного** (а не оценочного) счётчика reasoning-токенов на finish-step нужен `includeUsage: true` у `createOpenAICompatible` (см. ревью PR #177) — живой счётчик из стрима работает и без него. ## Критерии приёмки - [ ] При долгой фазе размышления (текста ответа ещё нет) виден раскрытый `ReasoningBlock`, наполняющийся в реальном времени. - [ ] Имя агента и лейаут не «прыгают» при переходе reasoning → ответный текст (нет дубля имени из индикатора и бабла). - [ ] После завершения turn'а reasoning остаётся доступен (сворачиваемый блок), переживает перезагрузку истории и попадает в markdown-экспорт — как сейчас. - [ ] Поведение консистентно на аутентифицированном чате и публичном share. - [ ] Сообщения, где reasoning пустой/только счётчик, не создают пустой 0-токенный блок (текущая защита сохранена). ## Заметка Стейл-комментарий: [message-item.tsx:41](apps/client/src/features/ai-chat/components/message-item.tsx#L41) гласит «reasoning … ignored for v1», хотя код ниже его рендерит — заодно поправить. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
vvzvlad added the feature label 2026-06-24 22:49:23 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#178