Skip to content

feat(rich-text): unified AI Enhance system with inline accept/reject UI#3919

Draft
siguenzaraul wants to merge 7 commits intomainfrom
claude/switch-rich-text-editor-TBIsz
Draft

feat(rich-text): unified AI Enhance system with inline accept/reject UI#3919
siguenzaraul wants to merge 7 commits intomainfrom
claude/switch-rich-text-editor-TBIsz

Conversation

@siguenzaraul
Copy link
Copy Markdown
Contributor

Summary

Replaces the old per-component Enhance UI (AcceptChanges, LoadingEnhance, EnhanceMenu, Error) with a unified AI Enhancement system shared via CoreEditor/Enhance/. This is a rebased and conflict-resolved continuation of #3394.

  • New CoreEditor/Enhance/ module — shared between RichTextEditor and NotesTextEditor
  • New useEnhance() hook — centralizes all enhance state (loading, accept/reject, error, undo/retry)
  • New EnhanceActivator — Radix Popover with smart geometry management, viewport locking during loading/review, and frozen position to prevent layout shift
  • New AIEnhanceMenu — 3-state menu (idle with conic-gradient animated input, loading with action status, review with accept/discard/retry) replacing the old separate AcceptChanges + LoadingEnhanceInline + Error components
  • Upgraded BubbleMenu — stays visible during enhance flow via shouldKeepEnhanceVisible, supports lockToViewportOnLock for fixed positioning in bubble context
  • Fullscreen toolbar with EnhanceRichTextEditor fullscreen mode now includes EnhanceActivator + ToolbarDivider alongside the standard toolbar
  • EnhancementOption.icon — options now support an optional IconType for richer dropdown rendering

Architecture

CoreEditor/
├── Enhance/
│   ├── EnhanceActivator.tsx   # Popover host with geometry freezing
│   ├── EnhanceMenu.tsx        # 3-state UI (idle / loading / review)
│   ├── useEnhance.ts          # React hook managing all enhance state
│   ├── enhance.ts             # Core logic (extract, context, apply, highlight)
│   ├── types.ts               # Shared types (enhanceConfig, EnhancementOption, etc.)
│   └── index.ts               # Barrel
├── Error/
│   └── index.tsx              # EnhanceErrorBanner (simplified, no editor prop)
├── BubbleMenu/                # Updated with Enhance integration
└── index.tsx                  # Barrel (now exports Enhance + Error)

Before (old flow)

RichTextEditor managed enhance state inline (useState x6) → rendered AcceptChanges, LoadingEnhanceInline, LoadingEnhanceOverlay, Error as separate animated sections → BubbleMenu hid entirely during enhance.

After (new flow)

RichTextEditor calls useEnhance(editor, config) → passes hook results to Footer, BubbleMenu, and fullscreen toolbar → EnhanceActivator opens AIEnhanceMenu popover which transitions through idle→loading→review states inline → BubbleMenu stays visible during enhance with locked geometry.

What's preserved from main

Removed

  • RichTextEditor/Enhance/AcceptChanges/ — replaced by review state in AIEnhanceMenu
  • RichTextEditor/Enhance/EnhanceMenu/ — replaced by CoreEditor/Enhance/EnhanceMenu
  • RichTextEditor/Enhance/LoadingEnhance/ — replaced by loading state in AIEnhanceMenu
  • RichTextEditor/Enhance/index.tsx — old EnhanceActivator (simpler, no geometry)
  • RichTextEditor/Error/index.tsx — replaced by CoreEditor/Error/EnhanceErrorBanner
  • RichTextEditor/utils/enhance.tsx — moved to CoreEditor/Enhance/enhance.ts

Test plan

  • Verify RichTextEditor renders correctly with and without enhanceConfig
  • Test enhance flow: select text → open AI menu → type custom prompt or pick option → verify loading state → accept/reject/retry
  • Test fullscreen mode: toolbar shows EnhanceActivator, enhance flow works in fullscreen
  • Test BubbleMenu stays visible during enhance loading/review and hides on error
  • Verify EnhanceErrorBanner shows on error and dismiss re-enables editor
  • Verify NotesTextEditor is unaffected (no regressions in pages, applyPageDocumentPatch)
  • Verify RichTextDisplay and FileItem are unaffected
  • Run pnpm tsc --noEmit — no new type errors

Context

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

…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
Copilot AI review requested due to automatic review settings April 12, 2026 21:21
@github-actions github-actions bot added feat react Changes affect packages/react labels Apr 12, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 12, 2026

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 12, 2026

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3919 to install the package

Use pnpm i github:factorialco/f0#abf68066aede2567cca8efd060a99c64235d678b to install this specific commit

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via CoreEditor.
  • Refactors RichTextEditor + Footer + BubbleMenu to use the unified enhance hook/state and keep the bubble menu visible through loading/review.
  • Removes the legacy per-component Enhance UI (AcceptChanges, LoadingEnhance*, old EnhanceMenu, old EnhanceActivator) 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.

Comment on lines +151 to +154
onClick={() => {
onSelect({
selectedIntent: undefined,
customIntent: searchQuery.trim(),
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
onClick={() => {
onSelect({
selectedIntent: undefined,
customIntent: searchQuery.trim(),
disabled={!searchQuery.trim()}
onClick={() => {
const trimmedSearchQuery = searchQuery.trim();
if (!trimmedSearchQuery) {
return;
}
onSelect({
selectedIntent: undefined,
customIntent: trimmedSearchQuery,

Copilot uses AI. Check for mistakes.
<>
<input
data-enhance-input="true"
type="text"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
type="text"
type="text"
aria-label="Custom enhancement request"

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +192
<F0Button
variant={useCompactReview ? "ghost" : "outline"}
icon={Reset}
label="Try again"
onClick={onRetry}
/>
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +205
<F0Button
variant={useCompactReview ? "ghost" : "outline"}
icon={Cross}
label="Discard"
onClick={onReject}
/>
<F0Button
variant={useCompactReview ? "ghost" : "default"}
icon={Check}
label="Accept"
onClick={onAccept}
/>
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More hardcoded user-facing strings ("Discard" / "Accept") should use the existing i18n keys (e.g., richTextEditor.ai.rejectChangesButtonLabel / acceptChangesButtonLabel) to avoid untranslated UI.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +176
<F0ActionItem title={loadingLabel} status="executing" />
<F0Button
variant="default"
icon={SolidStop}
label="Stop"
hideLabel
disabled
onClick={onRetry}
/>
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
onAcceptChanges?: () => void;
onRejectChanges?: () => void;
onRetryChanges?: () => void;
/** When true, the bubble menu is hidden (during loading, accept changes, or error) */
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/** 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). */

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 12, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 12, 2026

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 46.21% 11534 / 24959
🔵 Statements 45.44% 11894 / 26172
🔵 Functions 38.02% 2596 / 6827
🔵 Branches 37.97% 7579 / 19956
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/components/exports.ts 100% 100% 100% 100%
packages/react/src/components/RichText/exports.tsx 100% 100% 100% 100%
packages/react/src/components/RichText/internal/BubbleMenu/index.tsx 53.57% 34.28% 66.66% 50% 73-133
packages/react/src/components/RichText/internal/Enhance/EnhanceActivator.tsx 0.9% 0% 0% 0.95% 59-255
packages/react/src/components/RichText/internal/Enhance/EnhanceMenu.tsx 2.27% 0% 0% 2.38% 58-280
packages/react/src/components/RichText/internal/Enhance/index.ts 100% 100% 100% 100%
packages/react/src/components/RichText/internal/Enhance/types.ts 100% 100% 100% 100%
packages/react/src/components/RichText/internal/Enhance/useEnhance.ts 27.58% 17.24% 16.66% 30.18% 48, 53-55, 59, 62-64, 72-100, 108-112, 116-121, 125-128
packages/react/src/patterns/F0Form/fields/richtext/RichTextFieldRenderer.tsx 100% 100% 100% 100%
packages/react/src/patterns/F0Form/fields/richtext/types.ts 100% 100% 100% 100%
packages/react/src/sds/Home/Communities/Post/PostDescription/index.tsx 80% 100% 50% 80% 29-32
packages/react/src/sds/ai/Banners/F0AiBanner/AiBannerInternal.tsx 50% 0% 0% 50% 19-75, 80-110
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea/AttachedFilesList.tsx 16.66% 0% 0% 20% 20-40
packages/react/src/sds/ai/F0AiChat/components/messages/UserMessage.tsx 6.25% 0% 0% 6.66% 31-70, 82-116
Generated in workflow #12760 for commit 8178aad by the Vitest Coverage Report Action

claude added 2 commits April 12, 2026 21:30
… 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
Copilot AI review requested due to automatic review settings April 12, 2026 21:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

@github-actions github-actions bot added the react-native Changes affect packages/react-native label Apr 12, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 12, 2026

📱 Expo Go Preview Published

  • Branch: development

  • Message: `chore: fix oxfmt formatting and remove react-native cache files

  • 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`

  • Group ID: 21e64ffd-17b0-4beb-9bb4-bcbe1cf1a3e5
  • Created at: 2026-04-12T22:12:47.140Z

Links

QR Code

Expo Go Preview QR

Copilot AI review requested due to automatic review settings April 12, 2026 22:09
@github-actions github-actions bot removed the react-native Changes affect packages/react-native label Apr 12, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • F0NotesTextEditorComponent is created with forwardRef but doesn't set a displayName. Add F0NotesTextEditorComponent.displayName = "F0NotesTextEditor" (or similar) to match repo conventions and improve DevTools/debuggability.
    packages/react/src/components/RichText/F0RichTextEditor/index.tsx:97
  • F0RichTextEditorComponent is created with forwardRef but doesn't set a displayName. Add F0RichTextEditorComponent.displayName = "F0RichTextEditor" (or similar) to match repo conventions and improve DevTools/debuggability.
    packages/react/src/components/RichText/F0RichTextDisplay/index.tsx:34
  • _F0RichTextDisplay is created with forwardRef but doesn't set a displayName. Add a displayName to 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.tsx and uses render from @testing-library/react. In packages/react, tests should be .test.tsx and prefer zeroRender from @/testing/test-utils.tsx to match the established testing setup and avoid jsdom side effects.

Comment on lines +1 to +3
import * as Popover from "@radix-ui/react-popover"
import { AnimatePresence, motion } from "motion/react"
import { RefObject, useLayoutEffect, useRef, useState } from "react"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct Radix imports are discouraged in packages/react. Use the @/ui/popover wrapper instead of importing @radix-ui/react-popover directly.

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +154
<F0Button
variant="default"
icon={ArrowUp}
label="send"
hideLabel
disabled={!searchQuery.trim()}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +52
darkMode = false,
menuWidth,
menuState = "idle",
loadingLabel = "Thinking...",
onAccept,
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants