[bug][ui][dictation] Иконка диктовки залипает серой (disabled) после collab-досинка: DictationGroup читает editor.isEditable нереактивно #311

Closed
opened 2026-07-03 17:53:30 +03:00 by agent_vscode · 0 comments
Collaborator

Симптом

На странице, которую пользователь может редактировать (тело печатается, collab досинкался), иконка микрофона диктовки в байлайне (и в fixed-toolbar) остаётся серой/disabled. AI-кнопка «искры» рядом при этом активна. Наведение на серый микрофон ничего не поясняет (это отдельная проблема — см. #309).

Репро

  1. Открыть/создать страницу с включённой диктовкой (settings.ai.dictation = true) в режиме edit.
  2. Дождаться, пока тело станет редактируемым (collab-синк, showStatic → false).
  3. Микрофон в строке байлайна остаётся disabled, хотя тело нормально печатается.

Подтверждение диагноза / обходной путь: переключить режим view ↔ edit — PageByline перерендерится, editor.isEditable перечитается (уже true), и микрофон оживает.

Корень

DictationGroup берёт editable-состояние из нереактивного поля editor.isEditable прямо в рендере:

  • apps/client/src/features/editor/components/fixed-toolbar/groups/dictation-group.tsx:83disabled={!editor.isEditable}

editor приходит из pageEditorAtom (atom<Editor | null>, apps/client/src/features/editor/atoms/editor-atoms.ts:5) — это ссылка на объект редактора. Когда после синка вызывается editor.setEditable(true) (гейт #218, apps/client/src/features/editor/page-editor.tsx:465-472), меняется внутреннее состояние TipTap, но ссылка на объект та же → значение атома «не меняется» → подписчики (PageByline / DictationGroup) не перерендериваются.

Тело редактора эту же задачу решает правильно — через реактивный useEditorState:

  • apps/client/src/features/editor/page-editor.tsx:362-367

DictationGroup / MicButton useEditorState не используют → залипают на снимке isEditable, который был на момент их последнего рендера (в pre-sync окне = false).

Почему именно застревает: PageByline — сосед PageEditor, а не его потомок. Состояние showStatic живёт в локальном стейте PageEditor и наружу не выходит, pageEditorAtom не меняется (та же ссылка). Значит после досинка ничто не заставляет байлайн перерендериться, и disabled=true застревает навсегда, пока что-то другое (переключение режима, навигация) случайно не перерисует байлайн.

Ожидаемое поведение

Как только тело становится редактируемым, иконка диктовки должна автоматически активироваться (и деактивироваться обратно, когда тело теряет editable).

Фикс (точечный)

Читать editable реактивно — тем же приёмом, что и тело:

// dictation-group.tsx
import { Editor, useEditorState } from "@tiptap/react"; // было: import type { Editor } from "@tiptap/react";

export const DictationGroup: FC<Props> = ({ editor, color, iconSize }) => {
  // editor.isEditable is a mutable, non-reactive field — read it via
  // useEditorState so the mic re-enables when the body flips to editable
  // after collab sync (otherwise it stays stuck disabled).
  const isEditable = useEditorState({
    editor,
    selector: (ctx) => ctx.editor?.isEditable ?? false,
  });
  // ...
  return (
    <MicButton
      // ...
      disabled={!isEditable} // было: disabled={!editor.isEditable}
    />
  );
};

Чинит оба места, где рендерится DictationGroup: байлайн (apps/client/src/features/editor/full-editor.tsx:254) и fixed-toolbar. Исходное намерение #218 (нет диктовки до досинка) сохраняется — просто становится реактивным.

Затронутые файлы

  • apps/client/src/features/editor/components/fixed-toolbar/groups/dictation-group.tsx — правка.
  • Контекст: page-editor.tsx (гейт setEditable/showStatic), full-editor.tsx (PageByline), dictation/components/mic-button.tsx (проп disabled).

Связанное

  • #309 — говорящий tooltip / reason-модель на серой иконке (почему диктовка «молчит»). Данный баг — про то, почему иконка вообще «серая». Разные причины, чинятся независимо.
  • #218 — исходный pre-sync read-only гейт. Это не регресс самого гейта, а его нереактивное чтение в байлайне.
## Симптом На странице, которую пользователь **может** редактировать (тело печатается, collab досинкался), иконка микрофона диктовки в байлайне (и в fixed-toolbar) остаётся серой/`disabled`. AI-кнопка «искры» рядом при этом активна. Наведение на серый микрофон ничего не поясняет (это отдельная проблема — см. #309). ## Репро 1. Открыть/создать страницу с включённой диктовкой (`settings.ai.dictation = true`) в режиме edit. 2. Дождаться, пока тело станет редактируемым (collab-синк, `showStatic → false`). 3. Микрофон в строке байлайна остаётся `disabled`, хотя тело нормально печатается. Подтверждение диагноза / обходной путь: переключить режим view ↔ edit — `PageByline` перерендерится, `editor.isEditable` перечитается (уже `true`), и микрофон оживает. ## Корень `DictationGroup` берёт editable-состояние из **нереактивного** поля `editor.isEditable` прямо в рендере: - `apps/client/src/features/editor/components/fixed-toolbar/groups/dictation-group.tsx:83` → `disabled={!editor.isEditable}` `editor` приходит из `pageEditorAtom` (`atom<Editor | null>`, `apps/client/src/features/editor/atoms/editor-atoms.ts:5`) — это **ссылка на объект** редактора. Когда после синка вызывается `editor.setEditable(true)` (гейт #218, `apps/client/src/features/editor/page-editor.tsx:465-472`), меняется внутреннее состояние TipTap, но ссылка на объект та же → значение атома «не меняется» → подписчики (`PageByline` / `DictationGroup`) **не перерендериваются**. Тело редактора эту же задачу решает правильно — через реактивный `useEditorState`: - `apps/client/src/features/editor/page-editor.tsx:362-367` `DictationGroup` / `MicButton` `useEditorState` **не используют** → залипают на снимке `isEditable`, который был на момент их последнего рендера (в pre-sync окне = `false`). Почему именно застревает: `PageByline` — сосед `PageEditor`, а не его потомок. Состояние `showStatic` живёт в локальном стейте `PageEditor` и наружу не выходит, `pageEditorAtom` не меняется (та же ссылка). Значит после досинка ничто не заставляет байлайн перерендериться, и `disabled=true` застревает навсегда, пока что-то другое (переключение режима, навигация) случайно не перерисует байлайн. ## Ожидаемое поведение Как только тело становится редактируемым, иконка диктовки должна автоматически активироваться (и деактивироваться обратно, когда тело теряет editable). ## Фикс (точечный) Читать editable реактивно — тем же приёмом, что и тело: ```tsx // dictation-group.tsx import { Editor, useEditorState } from "@tiptap/react"; // было: import type { Editor } from "@tiptap/react"; export const DictationGroup: FC<Props> = ({ editor, color, iconSize }) => { // editor.isEditable is a mutable, non-reactive field — read it via // useEditorState so the mic re-enables when the body flips to editable // after collab sync (otherwise it stays stuck disabled). const isEditable = useEditorState({ editor, selector: (ctx) => ctx.editor?.isEditable ?? false, }); // ... return ( <MicButton // ... disabled={!isEditable} // было: disabled={!editor.isEditable} /> ); }; ``` Чинит оба места, где рендерится `DictationGroup`: байлайн (`apps/client/src/features/editor/full-editor.tsx:254`) и fixed-toolbar. Исходное намерение #218 (нет диктовки до досинка) сохраняется — просто становится реактивным. ## Затронутые файлы - `apps/client/src/features/editor/components/fixed-toolbar/groups/dictation-group.tsx` — правка. - Контекст: `page-editor.tsx` (гейт `setEditable`/`showStatic`), `full-editor.tsx` (`PageByline`), `dictation/components/mic-button.tsx` (проп `disabled`). ## Связанное - #309 — говорящий tooltip / reason-модель на серой иконке (почему диктовка «молчит»). Данный баг — про то, почему иконка вообще «серая». Разные причины, чинятся независимо. - #218 — исходный pre-sync read-only гейт. Это не регресс самого гейта, а его нереактивное чтение в байлайне.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#311