Skip to content

Text popover#1308

Merged
duckduckhero merged 5 commits intomainfrom
text-popover
Aug 10, 2025
Merged

Text popover#1308
duckduckhero merged 5 commits intomainfrom
text-popover

Conversation

@duckduckhero
Copy link
Contributor

@duckduckhero duckduckhero commented Aug 10, 2025

Summary by cubic

Added a text selection popover in enhanced notes that lets users analyze the source of selected text in the transcript.

  • New Features
    • Shows a popover when text is selected, offering a "Source" button.
    • Clicking "Source" opens an annotation box that streams an explanation and transcript quote related to the selected text.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 10, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

New annotation functionality is introduced to the editor area. This includes the addition of two new React components—AnnotationBox and TextSelectionPopover—and the integration of their state and handlers into the main editor component. These changes enable users to select text, trigger an annotation popover, and receive AI-generated explanations about the selected content.

Changes

Cohort / File(s) Change Summary
Annotation Popover Component
apps/desktop/src/components/editor-area/annotation-box.tsx
Introduces the AnnotationBox React component. This component displays a popover near selected text, streams AI-generated explanations about the selected text's source, manages cancellation, error handling, dynamic positioning, and UI updates in real time. It integrates transcript data, LLM streaming, and user interaction handling.
Editor Area Integration
apps/desktop/src/components/editor-area/index.tsx
Adds state and handlers to the EditorArea component to manage annotation popover visibility and selection. Integrates the new TextSelectionPopover and AnnotationBox components into the main editor UI, enabling annotation features without altering existing control flow.
Text Selection Popover Component
apps/desktop/src/components/editor-area/text-selection-popover.tsx
Adds the TextSelectionPopover React component. This component detects text selection, displays a contextual popover with annotation and (future) AI query options, manages selection state with debouncing, handles outside clicks and selection changes, and positions itself dynamically relative to the selected text.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant EditorArea
    participant TextSelectionPopover
    participant AnnotationBox
    participant LLM/Backend

    User->>EditorArea: Selects text in enhanced note
    EditorArea->>TextSelectionPopover: Renders popover if selection
    User->>TextSelectionPopover: Clicks "Annotate"
    TextSelectionPopover->>EditorArea: handleAnnotate(selectedText, rect)
    EditorArea->>AnnotationBox: Renders with selectedText, rect, sessionId
    AnnotationBox->>LLM/Backend: Streams explanation for selectedText
    LLM/Backend-->>AnnotationBox: Streamed explanation chunks
    AnnotationBox->>User: Displays streamed explanation in popover
    User->>AnnotationBox: Clicks close/cancel
    AnnotationBox->>EditorArea: onCancel()
    EditorArea->>AnnotationBox: Unmounts popover
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch text-popover

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic analysis

2 issues found across 3 files • Review in cubic

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
apps/desktop/src/components/editor-area/text-selection-popover.tsx (2)

57-57: UX nit: reduce debounce to feel snappier

600ms feels sluggish. 200–300ms is typical for selection popovers.

-        }, 600);
+        }, 250);

9-10: Comments should explain “why”, not “what”

Trim or reword these to convey intent (“why we hide when box is open”), not the obvious behavior.

Also applies to: 30-35, 88-91

apps/desktop/src/components/editor-area/index.tsx (3)

186-192: Consider storing a rect snapshot instead of DOMRect

DOMRect isn’t serializable and can be misleading in logs. A plain snapshot is simpler to reason about.

Example shape:

type Rect = Pick<DOMRect, "left" | "top" | "right" | "bottom" | "width" | "height">;
const [annotationBox, setAnnotationBox] = useState<{ selectedText: string; selectedRect: Rect } | null>(null);

…and construct the snapshot in handleAnnotate.


243-251: Comment style + potential scoping

  • The comment is “what”, not “why”. Suggest: “Why: keep onboarding UI minimal; avoid extra UI during first-run.”
  • Optional: if you ever support multiple editors on a page, pass a container/ref to TextSelectionPopover to scope selection to that editor.

252-259: Position won’t update on scroll/resize

AnnotationBox uses a static DOMRect. If the user scrolls or resizes, it can drift. Consider re-positioning on scroll/resize with a throttled effect.

apps/desktop/src/components/editor-area/annotation-box.tsx (2)

107-117: Guideline check: error handling

Coding guidelines for ts/tsx say “No error handling.” This block shows a toast and rethrows. Confirm if this is an approved exception for user feedback, or if error handling should be centralized elsewhere.


26-37: Comments are “what”, not “why”

Trim “CRITICAL/Cleanup/Start analysis/Cancel function/Header with close button” style comments. Keep only rationale where needed (e.g., “why fixed height”).

Also applies to: 159-165, 166-193, 253-269, 271-280

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0878189 and f6230f7.

📒 Files selected for processing (3)
  • apps/desktop/src/components/editor-area/annotation-box.tsx (1 hunks)
  • apps/desktop/src/components/editor-area/index.tsx (3 hunks)
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit Configuration File

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • apps/desktop/src/components/editor-area/text-selection-popover.tsx
  • apps/desktop/src/components/editor-area/annotation-box.tsx
  • apps/desktop/src/components/editor-area/index.tsx
🪛 Biome (2.1.2)
apps/desktop/src/components/editor-area/annotation-box.tsx

[error] 310-310: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 ast-grep (0.38.6)
apps/desktop/src/components/editor-area/annotation-box.tsx

[warning] 309-309: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🔇 Additional comments (2)
apps/desktop/src/components/editor-area/index.tsx (2)

193-200: LGTM on handlers

Simple and clear. No unnecessary complexity.


201-202: Good gating for enhanced content

This prevents the popover on raw/empty notes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
apps/desktop/src/components/editor-area/annotation-box.tsx (3)

309-316: Eliminate dangerouslySetInnerHTML; render safely to prevent XSS

LLM output is untrusted. Injecting via dangerouslySetInnerHTML is an XSS risk and violates lint/security guidance. Render as React nodes or sanitize first.

Apply in-place diff:

-                    <div
-                      className="prose prose-xs max-w-none"
-                      dangerouslySetInnerHTML={{
-                        __html: streamedContent
-                          .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
-                          .replace(/\n/g, "<br>"),
-                      }}
-                    />
+                    <div className="prose prose-xs max-w-none">
+                      {renderStreamedContent(streamedContent)}
+                    </div>

Add helper in this file (outside the selected range):

function renderStreamedContent(text: string) {
  if (!text) return null;

  const escapeHtml = (s: string) =>
    s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

  const renderBold = (line: string, keyPrefix: string) => {
    const out: (string | JSX.Element)[] = [];
    const re = /\*\*(.+?)\*\*/g;
    let last = 0, m: RegExpExecArray | null;
    while ((m = re.exec(line))) {
      if (m.index > last) out.push(line.slice(last, m.index));
      out.push(<strong key={`${keyPrefix}-b-${out.length}`}>{m[1]}</strong>);
      last = m.index + m[0].length;
    }
    if (last < line.length) out.push(line.slice(last));
    return out;
  };

  return escapeHtml(text).split("\n").map((ln, i, arr) => (
    <span key={`ln-${i}`}>
      {renderBold(ln, `ln-${i}`)}
      {i < arr.length - 1 ? <br /> : null}
    </span>
  ));
}

38-43: Prevent duplicate analysis on window focus; trigger mutate once via query onSuccess

Effect depends on llmConnectionQuery.data while query refetches on focus, causing repeated mutate calls.

Apply diffs:

  1. Disable refetch-on-focus and run mutate once in onSuccess:
   const llmConnectionQuery = useQuery({
     queryKey: ["llm-connection"],
     queryFn: () => connectorCommands.getLlmConnection(),
-    refetchOnWindowFocus: true,
+    refetchOnWindowFocus: false,
+    onSuccess: () => {
+      sourceAnalysisMutation.mutate();
+    },
   });
  1. Remove the effect that re-triggers mutate:
-  // Start analysis when component mounts
-  useEffect(() => {
-    if (llmConnectionQuery.data) {
-      sourceAnalysisMutation.mutate();
-    }
-  }, [llmConnectionQuery.data]);

Also applies to: 160-166


212-218: Fix top clamping; incorrect Math.min pins popover to margin

Clamp “below” to bottom margin and use Math.max for “above” to enforce a minimum top margin.

-    if (canFitBelow) {
-      // Prefer below if there's space for max height
-      top = selectedRect.bottom + minDistanceFromText;
-    } else if (canFitAbove) {
-      // Use above if below doesn't work but above does
-      top = selectedRect.top - maxPopoverHeight - minDistanceFromText;
+    if (canFitBelow) {
+      // Prefer below if there's space; clamp to bottom margin
+      top = Math.min(
+        selectedRect.bottom + minDistanceFromText,
+        window.innerHeight - margin - maxPopoverHeight,
+      );
+    } else if (canFitAbove) {
+      // Use above; clamp to top margin
+      top = Math.max(
+        margin,
+        selectedRect.top - maxPopoverHeight - minDistanceFromText,
+      );
     } else {
       // Neither fits perfectly - choose the side with more space
       if (spaceBelow >= spaceAbove) {
         // More space below - position to fit in available space
         top = Math.max(
           selectedRect.bottom + minDistanceFromText,
           window.innerHeight - Math.min(maxPopoverHeight, spaceBelow) - margin,
         );
       } else {
         // More space above - position to fit in available space
-        top = Math.min(
-          selectedRect.top - minDistanceFromText - maxPopoverHeight,
-          margin,
-        );
+        top = Math.max(
+          margin,
+          selectedRect.top - minDistanceFromText - maxPopoverHeight,
+        );
       }
     }

Also applies to: 227-231

🧹 Nitpick comments (3)
apps/desktop/src/components/editor-area/annotation-box.tsx (3)

53-60: Avoid redundant LLM connection fetch; reuse query data with fallback

You already fetch the connection via useQuery; calling connectorCommands.getLlmConnection() again is unnecessary.

-      const [{ type }, words] = await Promise.all([
-        connectorCommands.getLlmConnection(),
-        getWordsFunc(sessionId),
-      ]);
+      const [words] = await Promise.all([getWordsFunc(sessionId)]);
+      const type =
+        llmConnectionQuery.data?.type ??
+        (await connectorCommands.getLlmConnection()).type;

26-36: Trim comments; keep only “why”, not “what”

Several comments narrate implementation details (“CRITICAL”, “Create NEW abort controller”, header labels). Per guidelines, keep comments minimal and focused on rationale.

Suggestion:

  • Remove descriptive/obvious comments.
  • Keep a single brief comment explaining the rationale (e.g., “Why we combine abort + timeout”).

Also applies to: 49-52, 87-93, 98-106, 296-305


61-63: Bound prompt size; consider contextualization for long transcripts

Concatenating full transcript can exceed model/context limits and increase latency/cost.

Options (incremental):

  • Hard cap characters/tokens sent to the model.
  • Prefer contextualization: locate occurrences of selectedText (case-insensitive), send ±N words around top matches, plus a brief preface, instead of full transcript.
  • If needed, chunk transcript and stream retrieval progressively.

Example simple cap:

const MAX_CHARS = 12000;
const transcriptText = words.map(w => w.text).join(" ");
const promptTranscript = transcriptText.length > MAX_CHARS
  ? `${transcriptText.slice(0, MAX_CHARS)}…`
  : transcriptText;

Then use promptTranscript in userMessage.

Also applies to: 80-86

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6230f7 and 1dfdb43.

📒 Files selected for processing (2)
  • apps/desktop/src/components/editor-area/annotation-box.tsx (1 hunks)
  • apps/desktop/src/components/editor-area/index.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/components/editor-area/index.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit Configuration File

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • apps/desktop/src/components/editor-area/annotation-box.tsx
🪛 Biome (2.1.2)
apps/desktop/src/components/editor-area/annotation-box.tsx

[error] 311-311: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 ast-grep (0.38.6)
apps/desktop/src/components/editor-area/annotation-box.tsx

[warning] 310-310: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
🔇 Additional comments (1)
apps/desktop/src/components/editor-area/annotation-box.tsx (1)

107-116: Guideline check: "No error handling" policy for ts/tsx

Current code displays toasts and logs in onError paths. This appears to conflict with the repo guideline “No error handling” for ts/tsx.

Please confirm whether UI toasts/logging are permitted here. If not, remove the onError hooks and rely on upstream error boundaries/state (e.g., render a neutral fallback when streamedContent is empty). I can provide a diff once confirmed.

Also applies to: 144-157

@duckduckhero duckduckhero merged commit 06b5d60 into main Aug 10, 2025
7 checks passed
This was referenced Aug 27, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 5, 2025
@ComputelessComputer ComputelessComputer deleted the text-popover branch December 14, 2025 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant