feat(rich-text): unified AI Enhance system with inline accept/reject UI#3919
feat(rich-text): unified AI Enhance system with inline accept/reject UI#3919siguenzaraul wants to merge 7 commits intomainfrom
Conversation
…ect UI Port the new AI Enhancement architecture from PR #3394 onto the current main structure. The old per-component Enhance UI (AcceptChanges, LoadingEnhance, EnhanceMenu, Error) is replaced by a shared system in CoreEditor/Enhance/ that both RichTextEditor and NotesTextEditor can use. Key changes: - Add CoreEditor/Enhance/ with EnhanceActivator (Radix Popover with geometry management), AIEnhanceMenu (3-state: idle/loading/review), useEnhance hook, enhance.ts core logic, and shared types - Add CoreEditor/Error/EnhanceErrorBanner as a simplified error component - Upgrade BubbleMenu to integrate with the new Enhance system (keeps visible during loading/review, supports lockToViewportOnLock) - Refactor RichTextEditor to use useEnhance() hook instead of inline state management, add fullscreen toolbar with EnhanceActivator - Update Footer to accept new Enhance callback props - Add icon support to EnhancementOption type - Remove old RichTextEditor/Enhance/, Error/, and utils/enhance.tsx https://claude.ai/code/session_014jzD8EqZAveqHt6zkiDNrB
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
📦 Alpha Package Version PublishedUse Use |
There was a problem hiding this comment.
Pull request overview
This PR unifies the RichText “AI Enhance” experience by moving enhance logic/UI into a shared CoreEditor/Enhance module so both RichTextEditor and other editors can consume a single hook + popover-based UI.
Changes:
- Introduces shared
CoreEditor/Enhance(types,useEnhance,EnhanceActivator,AIEnhanceMenu) and re-exports it viaCoreEditor. - Refactors
RichTextEditor+Footer+BubbleMenuto use the unified enhance hook/state and keep the bubble menu visible through loading/review. - Removes the legacy per-component Enhance UI (
AcceptChanges,LoadingEnhance*, oldEnhanceMenu, oldEnhanceActivator) and simplifies the enhance error banner API.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/components/RichText/RichTextEditor/utils/types.tsx | Re-exports enhance types from CoreEditor/Enhance instead of defining them locally. |
| packages/react/src/components/RichText/RichTextEditor/index.tsx | Switches to useEnhance, integrates new activator/menu in fullscreen + footer + bubble menu, and uses new error banner. |
| packages/react/src/components/RichText/RichTextEditor/Footer/index.tsx | Uses CoreEditor’s EnhanceActivator and wires accept/reject/retry callbacks. |
| packages/react/src/components/RichText/RichTextEditor/Enhance/LoadingEnhance/index.tsx | Removes legacy loading UI (now handled by new menu states). |
| packages/react/src/components/RichText/RichTextEditor/Enhance/index.tsx | Removes legacy activator (replaced by CoreEditor/EnhanceActivator). |
| packages/react/src/components/RichText/RichTextEditor/Enhance/EnhanceMenu/index.tsx | Removes legacy menu implementation (replaced by CoreEditor/AIEnhanceMenu). |
| packages/react/src/components/RichText/RichTextEditor/Enhance/AcceptChanges/index.tsx | Removes legacy accept/reject UI (replaced by “review” state in new menu). |
| packages/react/src/components/RichText/CoreEditor/index.tsx | Exports the new Enhance and Error modules. |
| packages/react/src/components/RichText/CoreEditor/Error/index.tsx | Renames/simplifies enhance error banner API to EnhanceErrorBanner. |
| packages/react/src/components/RichText/CoreEditor/Enhance/useEnhance.ts | New central hook for enhance lifecycle: loading, review, error, accept/reject/retry. |
| packages/react/src/components/RichText/CoreEditor/Enhance/types.ts | New shared types for enhance config/options, including optional icons. |
| packages/react/src/components/RichText/CoreEditor/Enhance/index.ts | Barrel exports for enhance components/hook/types. |
| packages/react/src/components/RichText/CoreEditor/Enhance/EnhanceMenu.tsx | New 3-state menu UI (idle/loading/review) with dropdown options + prompt input. |
| packages/react/src/components/RichText/CoreEditor/Enhance/EnhanceActivator.tsx | New popover host with geometry freezing/viewport locking during loading/review. |
| packages/react/src/components/RichText/CoreEditor/Enhance/enhance.ts | Adjusts type imports to the new local types.ts. |
| packages/react/src/components/RichText/CoreEditor/BubbleMenu/index.tsx | Keeps bubble menu visible during enhance flow and integrates new activator props. |
| onClick={() => { | ||
| onSelect({ | ||
| selectedIntent: undefined, | ||
| customIntent: searchQuery.trim(), |
There was a problem hiding this comment.
The send button triggers onSelect even when searchQuery.trim() is empty, which can start an enhance request with no intent/custom prompt. Consider disabling the button (and/or returning early) until there is either a non-empty custom prompt or an option selection.
| onClick={() => { | |
| onSelect({ | |
| selectedIntent: undefined, | |
| customIntent: searchQuery.trim(), | |
| disabled={!searchQuery.trim()} | |
| onClick={() => { | |
| const trimmedSearchQuery = searchQuery.trim(); | |
| if (!trimmedSearchQuery) { | |
| return; | |
| } | |
| onSelect({ | |
| selectedIntent: undefined, | |
| customIntent: trimmedSearchQuery, |
| <> | ||
| <input | ||
| data-enhance-input="true" | ||
| type="text" |
There was a problem hiding this comment.
The text input relies on placeholder-only text and has no explicit accessible name. Add an aria-label (or equivalent) so screen readers announce the field correctly when the placeholder is not read/replaced by user input.
| type="text" | |
| type="text" | |
| aria-label="Custom enhancement request" |
| <F0Button | ||
| variant={useCompactReview ? "ghost" : "outline"} | ||
| icon={Reset} | ||
| label="Try again" | ||
| onClick={onRetry} | ||
| /> |
There was a problem hiding this comment.
Several user-facing strings are hardcoded here (e.g., "Try again"). This bypasses the existing richTextEditor.ai.* translations (see i18n-provider-defaults.ts) and will make the UI inconsistent across locales. Please use useI18n() strings (or add new keys if needed).
| <F0Button | ||
| variant={useCompactReview ? "ghost" : "outline"} | ||
| icon={Cross} | ||
| label="Discard" | ||
| onClick={onReject} | ||
| /> | ||
| <F0Button | ||
| variant={useCompactReview ? "ghost" : "default"} | ||
| icon={Check} | ||
| label="Accept" | ||
| onClick={onAccept} | ||
| /> |
There was a problem hiding this comment.
More hardcoded user-facing strings ("Discard" / "Accept") should use the existing i18n keys (e.g., richTextEditor.ai.rejectChangesButtonLabel / acceptChangesButtonLabel) to avoid untranslated UI.
| <F0ActionItem title={loadingLabel} status="executing" /> | ||
| <F0Button | ||
| variant="default" | ||
| icon={SolidStop} | ||
| label="Stop" | ||
| hideLabel | ||
| disabled | ||
| onClick={onRetry} | ||
| /> |
There was a problem hiding this comment.
In the loading state, the button is labeled "Stop" but is disabled and wired to onRetry. This is confusing and makes the control effectively dead code; either implement a real stop/cancel action (and enable the button) or remove/rename it to match the behavior.
| onAcceptChanges?: () => void; | ||
| onRejectChanges?: () => void; | ||
| onRetryChanges?: () => void; | ||
| /** When true, the bubble menu is hidden (during loading, accept changes, or error) */ |
There was a problem hiding this comment.
The enhanceActive prop comment says the bubble menu is hidden during loading/accept/error, but the current logic only uses it to hide on error (loading/review are kept visible via shouldKeepEnhanceVisible). Update the JSDoc to match the actual behavior to avoid confusion for future callers.
| /** When true, the bubble menu is hidden (during loading, accept changes, or error) */ | |
| /** When true, the bubble menu is hidden for the active enhance state (for example, on error). */ |
🔍 Visual review for your branch is published 🔍Here are the links to: |
… folders Follow the F0 naming convention from PR #3394: - CoreEditor/ → internal/ (shared infrastructure) - RichTextEditor/ → F0RichTextEditor/ - NotesTextEditor/ → F0NotesTextEditor/ - RichTextDisplay/ → F0RichTextDisplay/ - RichText/FileItem/ → components/F0FileItem/ (standalone) Component exports renamed: - RichTextEditor → F0RichTextEditor - NotesTextEditor → F0NotesTextEditor - RichTextDisplay → F0RichTextDisplay - FileItem → F0FileItem Also addresses Copilot review comments: - Use i18n strings for review labels (accept/reject/retry) - Disable send button when input is empty - Remove dead Stop button in loading state - Add aria-label to enhance text input - Fix BubbleMenu enhanceActive JSDoc https://claude.ai/code/session_014jzD8EqZAveqHt6zkiDNrB
📱 Expo Go Preview Published
https://claude.ai/code/session_014jzD8EqZAveqHt6zkiDNrB`
LinksQR Code |
- Run oxfmt on all src/ to fix 81 pre-existing formatting issues - Remove accidentally committed node-compile-cache files from react-native https://claude.ai/code/session_014jzD8EqZAveqHt6zkiDNrB
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 93 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (4)
packages/react/src/components/RichText/F0NotesTextEditor/index.tsx:75
F0NotesTextEditorComponentis created withforwardRefbut doesn't set adisplayName. AddF0NotesTextEditorComponent.displayName = "F0NotesTextEditor"(or similar) to match repo conventions and improve DevTools/debuggability.
packages/react/src/components/RichText/F0RichTextEditor/index.tsx:97F0RichTextEditorComponentis created withforwardRefbut doesn't set adisplayName. AddF0RichTextEditorComponent.displayName = "F0RichTextEditor"(or similar) to match repo conventions and improve DevTools/debuggability.
packages/react/src/components/RichText/F0RichTextDisplay/index.tsx:34_F0RichTextDisplayis created withforwardRefbut doesn't set adisplayName. Add adisplayNameto align with repo conventions and improve React DevTools/debug output.
packages/react/src/components/RichText/F0RichTextEditor/index.spec.tsx:3- This test file is named
index.spec.tsxand usesrenderfrom@testing-library/react. Inpackages/react, tests should be.test.tsxand preferzeroRenderfrom@/testing/test-utils.tsxto match the established testing setup and avoid jsdom side effects.
| import * as Popover from "@radix-ui/react-popover" | ||
| import { AnimatePresence, motion } from "motion/react" | ||
| import { RefObject, useLayoutEffect, useRef, useState } from "react" |
There was a problem hiding this comment.
Direct Radix imports are discouraged in packages/react. Use the @/ui/popover wrapper instead of importing @radix-ui/react-popover directly.
| <F0Button | ||
| variant="default" | ||
| icon={ArrowUp} | ||
| label="send" | ||
| hideLabel | ||
| disabled={!searchQuery.trim()} |
There was a problem hiding this comment.
The button label is hardcoded as "send". Since this is user-facing text, it should come from useI18n() (and ideally match existing action labels like i18n.actions.send/i18n.actions.submit if available).
| darkMode = false, | ||
| menuWidth, | ||
| menuState = "idle", | ||
| loadingLabel = "Thinking...", | ||
| onAccept, |
There was a problem hiding this comment.
loadingLabel defaults to the hardcoded string "Thinking...". Since this is user-facing, it should default to an i18n string (or require the caller to pass the label) to keep the component localized by default.
…ooter Critical fixes to match PR #3394 functionality: - Add enhanceConfig prop + useEnhance hook to F0NotesTextEditor - Add EnhanceHighlight extension with enhanceEnabled flag to NotesTextEditor extensions (required for highlight commands) - Render EnhanceErrorBanner with AnimatePresence in NotesTextEditor - Wire EditorBubbleMenu with all enhance props in NotesTextEditor - Add menuWidth/menuContainerRef to Footer's EnhanceActivator for proper menu sizing/positioning - Hide Footer's EnhanceActivator in fullscreen (avoid duplicates with the fullscreen toolbar's activator) - Use i18n for send button label and loading label default - Export enhanceConfig + EnhancementOption types from NotesTextEditor https://claude.ai/code/session_014jzD8EqZAveqHt6zkiDNrB
Summary
Replaces the old per-component Enhance UI (
AcceptChanges,LoadingEnhance,EnhanceMenu,Error) with a unified AI Enhancement system shared viaCoreEditor/Enhance/. This is a rebased and conflict-resolved continuation of #3394.CoreEditor/Enhance/module — shared betweenRichTextEditorandNotesTextEditoruseEnhance()hook — centralizes all enhance state (loading, accept/reject, error, undo/retry)EnhanceActivator— Radix Popover with smart geometry management, viewport locking during loading/review, and frozen position to prevent layout shiftAIEnhanceMenu— 3-state menu (idle with conic-gradient animated input, loading with action status, review with accept/discard/retry) replacing the old separateAcceptChanges+LoadingEnhanceInline+ErrorcomponentsBubbleMenu— stays visible during enhance flow viashouldKeepEnhanceVisible, supportslockToViewportOnLockfor fixed positioning in bubble contextRichTextEditorfullscreen mode now includesEnhanceActivator+ToolbarDivideralongside the standard toolbarEnhancementOption.icon— options now support an optionalIconTypefor richer dropdown renderingArchitecture
Before (old flow)
RichTextEditormanaged enhance state inline (useStatex6) → renderedAcceptChanges,LoadingEnhanceInline,LoadingEnhanceOverlay,Erroras separate animated sections →BubbleMenuhid entirely during enhance.After (new flow)
RichTextEditorcallsuseEnhance(editor, config)→ passes hook results toFooter,BubbleMenu, and fullscreen toolbar →EnhanceActivatoropensAIEnhanceMenupopover which transitions through idle→loading→review states inline →BubbleMenustays visible during enhance with locked geometry.What's preserved from main
applyPageDocumentPatch+ tests inNotesTextEditor(from feat(rich-text): add AI patch finalization API and harden BlockIdExtension (FCT-50714) #3781)BlockIdExtensionhardening withdocumentHasMissingBlockIdsDataTestIdWrapper,onBlur,errorprop,loading,dataTestIdonRichTextEditorFileItemwithcvavariants andwithDataTestIdCoreEditor/, no F0 prefix)Removed
RichTextEditor/Enhance/AcceptChanges/— replaced by review state inAIEnhanceMenuRichTextEditor/Enhance/EnhanceMenu/— replaced byCoreEditor/Enhance/EnhanceMenuRichTextEditor/Enhance/LoadingEnhance/— replaced by loading state inAIEnhanceMenuRichTextEditor/Enhance/index.tsx— oldEnhanceActivator(simpler, no geometry)RichTextEditor/Error/index.tsx— replaced byCoreEditor/Error/EnhanceErrorBannerRichTextEditor/utils/enhance.tsx— moved toCoreEditor/Enhance/enhance.tsTest plan
RichTextEditorrenders correctly with and withoutenhanceConfigEnhanceActivator, enhance flow works in fullscreenBubbleMenustays visible during enhance loading/review and hides on errorEnhanceErrorBannershows on error and dismiss re-enables editorNotesTextEditoris unaffected (no regressions in pages, applyPageDocumentPatch)RichTextDisplayandFileItemare unaffectedpnpm tsc --noEmit— no new type errorsContext
Continuation of #3394 (rebased onto current main to resolve structural conflicts from the
internal/→CoreEditor/rename and F0 prefix removal).https://claude.ai/code/session_014jzD8EqZAveqHt6zkiDNrB