Skip to content

Key Factors Redesign 3d iteration#4548

Open
ncarazon wants to merge 3 commits intomainfrom
feat/key-factors-3d-iteration
Open

Key Factors Redesign 3d iteration#4548
ncarazon wants to merge 3 commits intomainfrom
feat/key-factors-3d-iteration

Conversation

@ncarazon
Copy link
Copy Markdown
Contributor

@ncarazon ncarazon commented Mar 26, 2026

This PR implements the 3d iteration of the Key Factors Redesign

Iteration 3: Timeline Annotations

  • News-type key factors plotted on the timeline chart X axis
  • Toggle annotations on/off, stacked annotations for grouped time points
  • Requires integration with the timeline/chart component
image image image

Summary by CodeRabbit

  • New Features

    • Timeline charts now show clustered news markers with popups and inline annotation cards; a toggle lets you show/hide news annotations (UI text added in multiple locales).
  • Refactor

    • Timelines and detail views surface related key-factor/news data so annotations appear on question timelines.
    • Chart headers can host additional controls (e.g., the annotations toggle).
  • Style

    • Improved scroll behavior in key-factor comment panels via custom overlay scrollbars.

@ncarazon ncarazon force-pushed the feat/key-factors-3d-iteration branch from 9450f3f to f7cf7ec Compare March 26, 2026 15:35
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f67fbb00-d196-4bff-b14e-a6d3fe887637

📥 Commits

Reviewing files that changed from the base of the PR and between 6acf0f3 and a656bb7.

📒 Files selected for processing (1)
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx
✅ Files skipped from review due to trivial changes (1)
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx

📝 Walkthrough

Walkthrough

Threaded key_factors into timeline and detailed-question components and added news-annotation primitives (types, builders, clustering, markers, toggle, popup, and chart integration) to overlay interactive news markers on question timelines.

Changes

Cohort / File(s) Summary
Question views / layout
front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/index.tsx, front_end/src/app/(main)/questions/[id]/components/question_layout/question_info.tsx, front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/timeline/index.tsx, front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/index.tsx
Forward postData.key_factors/keyFactors into timeline and detailed card components; add optional keyFactors prop to timeline component signatures.
Chart components & container
front_end/src/components/charts/numeric_chart.tsx, front_end/src/components/charts/numeric_timeline.tsx, front_end/src/components/charts/primitives/chart_container.tsx
Extend chart props to accept news-annotation data and toggle handlers; compute bottom padding and first-tick offset; compute positioned clusters and render TimelineNewsAnnotations overlay; add headerExtra slot for toggle UI.
Detailed question card integration
front_end/src/components/detailed_question_card/detailed_question_card/index.tsx, front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx
Add optional keyFactors prop; add local showNewsAnnotations state and detect presence of news key factors; forward news-related props to NumericTimeline.
News annotation UI components
front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx, .../news_annotation_toggle.tsx, .../news_annotation_popup.tsx, .../news_annotation_popup_card.tsx, .../timeline_news_annotations.tsx
Add marker, toggle, popup, popup-card, and timeline overlay components with hover/click behavior, floating positioning, popup lifecycle, and marker-count UI.
Annotation types & utilities
front_end/src/components/charts/primitives/news_annotations/types.ts, front_end/src/components/charts/primitives/news_annotations/utils.ts
Introduce NewsAnnotation and AnnotationCluster types; add buildNewsAnnotations (extract from KeyFactors) and clusterAnnotations (group by day & enforce min pixel separation).
Key-factor UX / analytics
front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/index.tsx, front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/key_factor_strength_item.tsx
Emit analytics events for key-factor vote actions (downvote reasons and impact/strength votes); make analytics emission conditional to avoid duplicates.
Scrollbars / UI deps
front_end/src/app/(main)/questions/[id]/components/key_factors/comment_detail_panel.tsx, front_end/package.json
Replace native scroll div with OverlayScrollbarsComponent, import its stylesheet, and add overlayscrollbars + overlayscrollbars-react dependencies.
i18n
front_end/messages/en.json, front_end/messages/cs.json, front_end/messages/es.json, front_end/messages/pt.json, front_end/messages/zh-TW.json, front_end/messages/zh.json
Add toggleNewsAnnotations translation key in multiple locales for the news-annotation toggle UI.
Other detailed components
front_end/src/components/charts/primitives/news_annotations/* (new files), front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx, front_end/src/components/detailed_question_card/detailed_question_card/index.tsx
New news-annotation primitives, wiring into detailed card and timeline rendering; prop and type additions across related components.

Sequence Diagram

sequenceDiagram
    participant User
    participant Chart as NumericChart / NumericTimeline
    participant TimelineOverlay as TimelineNewsAnnotations
    participant Marker as NewsAnnotationMarker
    participant Floating as FloatingUI / FloatingPortal
    participant Popup as NewsAnnotationPopup

    User->>Chart: open timeline (with keyFactors)
    Chart->>TimelineOverlay: provide positioned clusters, chartHeight, axis offset
    User->>Marker: hover / click marker at xPixel
    Marker->>TimelineOverlay: onHoverStart / onClick (cluster id)
    TimelineOverlay->>Floating: compute placement (useFloating)
    Floating-->>TimelineOverlay: position & merged styles
    TimelineOverlay->>Popup: open popup for cluster (render up to 3 cards)
    User->>Popup: interact (click key-factor / external link / outside)
    Popup->>TimelineOverlay: close request (hover out / escape / outside click)
    TimelineOverlay->>Marker: clear active cluster id
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • elisescu
  • lsabor
  • hlbmtc

Poem

🐰 I hopped along the timeline bright,

tiny markers glowed with newsy light.
Hover made popups sprout like clover,
charts and factors chatting over.
A rabbit cheers — new features, what a sight!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Key Factors Redesign 3d iteration' directly references the main feature being implemented—the 3rd iteration of a Key Factors design overhaul focused on timeline annotations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/key-factors-3d-iteration

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4548-feat-key-factors-3d-iteration-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-key-factors-3d-iteration-a656bb7
🗄️ PostgreSQL NeonDB branch preview/pr-4548-feat-key-factors-3d-iteration
Redis Fly Redis mtc-redis-pr-4548-feat-key-factors-3d-iteration

Details

  • Commit: 0db7938cbdea6190670c6e835cd267552ea635b6
  • Branch: feat/key-factors-3d-iteration
  • Fly App: metaculus-pr-4548-feat-key-factors-3d-iteration

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

@ncarazon ncarazon force-pushed the feat/key-factors-2nd-iteration branch 2 times, most recently from d7a8288 to d35a854 Compare March 30, 2026 12:16
Base automatically changed from feat/key-factors-2nd-iteration to main March 30, 2026 12:33
@ncarazon ncarazon force-pushed the feat/key-factors-3d-iteration branch from f7cf7ec to daa579c Compare March 30, 2026 12:37
Copy link
Copy Markdown
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/src/components/charts/primitives/chart_container.tsx (1)

50-51: ⚠️ Potential issue | 🟠 Major

headerExtra is unintentionally hidden when chartTitle and zoom are absent.

On Line 50, the header is gated by !!chartTitle || !!zoom, so headerExtra-only usage never renders. Include headerExtra in the outer condition to make the new slot reliable.

Suggested fix
-        {(!!chartTitle || !!zoom) && !isEmbed && (
+        {(!!chartTitle || !!zoom || !!headerExtra) && !isEmbed && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front_end/src/components/charts/primitives/chart_container.tsx` around lines
50 - 51, The header block is currently only rendered when !!chartTitle || !!zoom
and thus hides headerExtra-only usage; update the outer conditional that renders
the header div (the JSX guarded by (!!chartTitle || !!zoom) && !isEmbed) to also
check for headerExtra (e.g., (!!chartTitle || !!zoom || !!headerExtra) &&
!isEmbed) so that headerExtra is shown even if chartTitle and zoom are absent;
adjust references to chartTitle, zoom, headerExtra, and isEmbed accordingly.
♻️ Duplicate comments (1)
front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx (1)

24-24: ⚠️ Potential issue | 🟡 Minor

Hardcoded aria-label should use i18n.

The aria-label="Toggle news annotations" is a hardcoded English string. Per project conventions, use useTranslations() to support localization.

Based on learnings: "Do not hardcode English strings in TSX components... Prefer using useTranslations() and i18n strings for UI text across components."

🌐 Proposed fix for i18n
+"use client";
+
+import { faNewspaper } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useTranslations } from "next-intl";
+import { FC } from "react";
+
+import cn from "@/utils/core/cn";
+
 const NewsAnnotationToggle: FC<Props> = ({ enabled, onToggle }) => {
+  const t = useTranslations();
   return (
     <button
       className={cn(...)}
       onClick={onToggle}
-      aria-label="Toggle news annotations"
+      aria-label={t("toggleNewsAnnotations")}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx`
at line 24, The aria-label on the NewsAnnotationToggle component is hardcoded
("Toggle news annotations"); change it to use the i18n hook useTranslations and
a translation key instead: import and call useTranslations within the
NewsAnnotationToggle (or the component that renders the toggle), replace the
static aria-label with the returned localized string (e.g.,
t('news.toggleAnnotations') or your project's key) so the rendered
button/checkbox uses the translated label; update or add the corresponding i18n
entry for that key in your locale files.
🧹 Nitpick comments (2)
front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx (1)

38-51: Consider showing a count indicator when annotations are truncated.

When a cluster has more than 3 annotations, users see only the first 3 with no indication that more exist. Consider adding a "+N more" indicator or similar UI feedback.

💡 Optional: Add truncation indicator
   const visibleAnnotations = cluster.annotations.slice(
     0,
     MAX_VISIBLE_ANNOTATIONS
   );
+  const hiddenCount = cluster.annotations.length - visibleAnnotations.length;

   return (
     <div ref={ref} className="flex w-[280px] flex-col gap-[5px] rounded-xl p-0">
       {visibleAnnotations.map((annotation) => (
         <NewsAnnotationPopupCard
           key={annotation.keyFactor.id}
           annotation={annotation}
           questionType={questionType}
         />
       ))}
+      {hiddenCount > 0 && (
+        <div className="text-center text-xs text-gray-500">
+          +{hiddenCount} more
+        </div>
+      )}
     </div>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx`
around lines 38 - 51, The list currently slices cluster.annotations into
visibleAnnotations (using MAX_VISIBLE_ANNOTATIONS) and renders
NewsAnnotationPopupCard for each, but provides no indication when more exist;
compute remainingCount = cluster.annotations.length - visibleAnnotations.length
and, if remainingCount > 0, render a small truncation indicator (e.g., a
"+{remainingCount} more" element) after the mapped NewsAnnotationPopupCard items
inside the same container; ensure the indicator uses the same styling/spacing as
the cards and include an accessible label (aria-label) so screen readers know
there are additional annotations.
front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx (1)

92-98: Consider adding loading="lazy" for thumbnail images.

For better performance, especially when multiple annotation cards are rendered, lazy loading the thumbnail images can reduce initial load time.

⚡ Optional performance improvement
             {imgUrl && (
               <img
                 className="size-10 rounded object-cover"
                 src={imgUrl}
                 alt={title}
+                loading="lazy"
               />
             )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx`
around lines 92 - 98, Add native lazy-loading to the thumbnail <img> rendered
when imgUrl is present in the NewsAnnotationPopupCard component: update the JSX
block that checks imgUrl (the element using src={imgUrl} alt={title} and
className="size-10 rounded object-cover") to include loading="lazy" so the
browser defers offscreen image decoding; keep existing props (src, alt,
className) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx`:
- Around line 21-35: The marker button in news_annotation_marker.tsx (the button
rendering inside the NewsAnnotationMarker component that uses props isActive,
count, and onClick) is missing an explicit type and accessible name/state;
update the <button> to include type="button", add an accessible name
(aria-label) that reflects its content (e.g., include count when count>1 and a
description when showing the newspaper icon), and expose its pressed state via
aria-pressed={isActive} so assistive tech can convey active/inactive; ensure the
aria-label text is derived from the existing count/isActive values and keep
FontAwesomeIcon usage unchanged.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx`:
- Around line 57-64: The interactive card uses a non-semantic div with only
onClick (see url and handleClick in NewsAnnotationPopupCard), which is not
keyboard-accessible; update the component so when url is present either wrap the
card in an anchor element or replace the div with a semantic interactive element
(e.g., <a> or <button>) and ensure keyboard handlers and accessibility
attributes are present: make it focusable (tabIndex if needed), handle
Enter/Space in onKeyDown to call handleClick, and add an appropriate
aria-label/role to describe the action.

In `@front_end/src/components/charts/primitives/news_annotations/utils.ts`:
- Around line 82-93: The current loop in the clusters spacing pass (using
clusters[], MIN_MARKER_GAP and xPixel) adjusts both prev.xPixel and curr.xPixel
by a midpoint which can break spacing with earlier neighbors; change it to a
monotonic forward push: iterate i from 1..clusters.length-1, compute prev =
clusters[i-1] and curr = clusters[i], and if curr.xPixel < prev.xPixel +
MIN_MARKER_GAP set curr.xPixel = prev.xPixel + MIN_MARKER_GAP (only move the
current cluster forward) so each update preserves the minimum gap with all
previous clusters.
- Around line 19-21: The timestamp computation uses new Date(news.published_at)
without validating the parsed date, which yields NaN for malformed strings and
breaks downstream math; modify the logic in the timestamp assignment to parse
news.published_at into a Date object, check validity with
isNaN(parsed.getTime()) (or Number.isNaN), and only use its Unix seconds when
valid; otherwise fall back to parsing kf.created_at (also validate it) and
finally fallback to Math.floor(Date.now()/1000) if both dates are invalid—update
the code around the timestamp constant and references to news.published_at and
kf.created_at accordingly.

---

Outside diff comments:
In `@front_end/src/components/charts/primitives/chart_container.tsx`:
- Around line 50-51: The header block is currently only rendered when
!!chartTitle || !!zoom and thus hides headerExtra-only usage; update the outer
conditional that renders the header div (the JSX guarded by (!!chartTitle ||
!!zoom) && !isEmbed) to also check for headerExtra (e.g., (!!chartTitle ||
!!zoom || !!headerExtra) && !isEmbed) so that headerExtra is shown even if
chartTitle and zoom are absent; adjust references to chartTitle, zoom,
headerExtra, and isEmbed accordingly.

---

Duplicate comments:
In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx`:
- Line 24: The aria-label on the NewsAnnotationToggle component is hardcoded
("Toggle news annotations"); change it to use the i18n hook useTranslations and
a translation key instead: import and call useTranslations within the
NewsAnnotationToggle (or the component that renders the toggle), replace the
static aria-label with the returned localized string (e.g.,
t('news.toggleAnnotations') or your project's key) so the rendered
button/checkbox uses the translated label; update or add the corresponding i18n
entry for that key in your locale files.

---

Nitpick comments:
In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx`:
- Around line 92-98: Add native lazy-loading to the thumbnail <img> rendered
when imgUrl is present in the NewsAnnotationPopupCard component: update the JSX
block that checks imgUrl (the element using src={imgUrl} alt={title} and
className="size-10 rounded object-cover") to include loading="lazy" so the
browser defers offscreen image decoding; keep existing props (src, alt,
className) unchanged.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx`:
- Around line 38-51: The list currently slices cluster.annotations into
visibleAnnotations (using MAX_VISIBLE_ANNOTATIONS) and renders
NewsAnnotationPopupCard for each, but provides no indication when more exist;
compute remainingCount = cluster.annotations.length - visibleAnnotations.length
and, if remainingCount > 0, render a small truncation indicator (e.g., a
"+{remainingCount} more" element) after the mapped NewsAnnotationPopupCard items
inside the same container; ensure the indicator uses the same styling/spacing as
the cards and include an accessible label (aria-label) so screen readers know
there are additional annotations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2bec821b-4d1e-4a1e-a227-e9f880486ec3

📥 Commits

Reviewing files that changed from the base of the PR and between e04276e and daa579c.

📒 Files selected for processing (16)
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_info.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/timeline/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/index.tsx
  • front_end/src/components/charts/numeric_chart.tsx
  • front_end/src/components/charts/numeric_timeline.tsx
  • front_end/src/components/charts/primitives/chart_container.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx
  • front_end/src/components/charts/primitives/news_annotations/timeline_news_annotations.tsx
  • front_end/src/components/charts/primitives/news_annotations/types.ts
  • front_end/src/components/charts/primitives/news_annotations/utils.ts
  • front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx
  • front_end/src/components/detailed_question_card/detailed_question_card/index.tsx

Comment on lines +57 to +64
return (
<div
className={cn(
"dark flex gap-3 rounded-xl bg-blue-800 p-4 dark:bg-blue-300-dark",
url && "cursor-pointer"
)}
onClick={handleClick}
>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clickable div lacks keyboard accessibility.

The card is interactive when url is present but uses a div with only onClick. This is not accessible to keyboard users. Consider using a proper interactive element or adding ARIA attributes.

♿ Proposed fix for accessibility
   return (
     <div
       className={cn(
         "dark flex gap-3 rounded-xl bg-blue-800 p-4 dark:bg-blue-300-dark",
         url && "cursor-pointer"
       )}
       onClick={handleClick}
+      role={url ? "button" : undefined}
+      tabIndex={url ? 0 : undefined}
+      onKeyDown={url ? (e) => {
+        if (e.key === "Enter" || e.key === " ") {
+          e.preventDefault();
+          handleClick();
+        }
+      } : undefined}
     >

Alternatively, wrap the entire card in an <a> tag when url is present.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<div
className={cn(
"dark flex gap-3 rounded-xl bg-blue-800 p-4 dark:bg-blue-300-dark",
url && "cursor-pointer"
)}
onClick={handleClick}
>
return (
<div
className={cn(
"dark flex gap-3 rounded-xl bg-blue-800 p-4 dark:bg-blue-300-dark",
url && "cursor-pointer"
)}
onClick={handleClick}
role={url ? "button" : undefined}
tabIndex={url ? 0 : undefined}
onKeyDown={url ? (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
} : undefined}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx`
around lines 57 - 64, The interactive card uses a non-semantic div with only
onClick (see url and handleClick in NewsAnnotationPopupCard), which is not
keyboard-accessible; update the component so when url is present either wrap the
card in an anchor element or replace the div with a semantic interactive element
(e.g., <a> or <button>) and ensure keyboard handlers and accessibility
attributes are present: make it focusable (tabIndex if needed), handle
Enter/Space in onKeyDown to call handleClick, and add an appropriate
aria-label/role to describe the action.

@ncarazon ncarazon force-pushed the feat/key-factors-3d-iteration branch from daa579c to cdfdc61 Compare March 31, 2026 08:21
Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx (1)

251-257: Consider memoizing the toggle callback.

The inline arrow function () => setShowNewsAnnotations((prev) => !prev) creates a new reference on each render. While not critical since it's only used when the toggle is visible, wrapping it in useCallback would be more consistent with the other callbacks in this component.

💡 Optional optimization

Add a memoized callback alongside handleCursorChange and handleChartReady:

+  const handleToggleNewsAnnotations = useCallback(() => {
+    setShowNewsAnnotations((prev) => !prev);
+  }, []);

   // ... in renderTimeline:
       onToggleNewsAnnotations={
         hasNewsKeyFactors
-          ? () => setShowNewsAnnotations((prev) => !prev)
+          ? handleToggleNewsAnnotations
           : undefined
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx`
around lines 251 - 257, The inline toggle handler for news annotations should be
memoized: create a useCallback named (for example) toggleNewsAnnotations that
calls setShowNewsAnnotations(prev => !prev) and include setShowNewsAnnotations
in its dependency array, then pass hasNewsKeyFactors ? toggleNewsAnnotations :
undefined to onToggleNewsAnnotations; this matches the memoization pattern used
for handleCursorChange and handleChartReady and prevents creating a new function
each render.
front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx (1)

38-51: Consider indicating when annotations are truncated.

When a cluster has more than 3 annotations, users see only the first 3 with no indication that more exist. Consider adding a "+N more" indicator.

💡 Proposed enhancement
   const visibleAnnotations = cluster.annotations.slice(
     0,
     MAX_VISIBLE_ANNOTATIONS
   );
+  const hiddenCount = cluster.annotations.length - visibleAnnotations.length;

   return (
     <div ref={ref} className="flex w-[280px] flex-col gap-[5px] rounded-xl p-0">
       {visibleAnnotations.map((annotation) => (
         <NewsAnnotationPopupCard
           key={annotation.keyFactor.id}
           annotation={annotation}
           questionType={questionType}
         />
       ))}
+      {hiddenCount > 0 && (
+        <div className="text-center text-sm text-gray-600">
+          +{hiddenCount} more
+        </div>
+      )}
     </div>
   );

Note: The "+N more" text should use useTranslations() for i18n compliance. Based on learnings, avoid hardcoding English strings in TSX components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx`
around lines 38 - 51, The popup currently slices cluster.annotations into
visibleAnnotations (using MAX_VISIBLE_ANNOTATIONS) and renders
NewsAnnotationPopupCard for each, but provides no UI that more items exist;
update the component to compute remaining = cluster.annotations.length -
MAX_VISIBLE_ANNOTATIONS and, when remaining > 0, render a localized "+N more"
indicator (use useTranslations() and a plural/count-safe key, e.g.
t('news.moreAnnotations', { count: remaining })) placed after the mapped
NewsAnnotationPopupCard list so users know there are hidden annotations; ensure
you reference visibleAnnotations, MAX_VISIBLE_ANNOTATIONS, cluster, and
NewsAnnotationPopupCard when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx`:
- Around line 38-51: The popup currently slices cluster.annotations into
visibleAnnotations (using MAX_VISIBLE_ANNOTATIONS) and renders
NewsAnnotationPopupCard for each, but provides no UI that more items exist;
update the component to compute remaining = cluster.annotations.length -
MAX_VISIBLE_ANNOTATIONS and, when remaining > 0, render a localized "+N more"
indicator (use useTranslations() and a plural/count-safe key, e.g.
t('news.moreAnnotations', { count: remaining })) placed after the mapped
NewsAnnotationPopupCard list so users know there are hidden annotations; ensure
you reference visibleAnnotations, MAX_VISIBLE_ANNOTATIONS, cluster, and
NewsAnnotationPopupCard when making the change.

In
`@front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx`:
- Around line 251-257: The inline toggle handler for news annotations should be
memoized: create a useCallback named (for example) toggleNewsAnnotations that
calls setShowNewsAnnotations(prev => !prev) and include setShowNewsAnnotations
in its dependency array, then pass hasNewsKeyFactors ? toggleNewsAnnotations :
undefined to onToggleNewsAnnotations; this matches the memoization pattern used
for handleCursorChange and handleChartReady and prevents creating a new function
each render.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 88b20820-f7b5-49a7-8d39-7f99a123ae5e

📥 Commits

Reviewing files that changed from the base of the PR and between daa579c and cdfdc61.

📒 Files selected for processing (16)
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_info.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/timeline/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/index.tsx
  • front_end/src/components/charts/numeric_chart.tsx
  • front_end/src/components/charts/numeric_timeline.tsx
  • front_end/src/components/charts/primitives/chart_container.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx
  • front_end/src/components/charts/primitives/news_annotations/timeline_news_annotations.tsx
  • front_end/src/components/charts/primitives/news_annotations/types.ts
  • front_end/src/components/charts/primitives/news_annotations/utils.ts
  • front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx
  • front_end/src/components/detailed_question_card/detailed_question_card/index.tsx
✅ Files skipped from review due to trivial changes (4)
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/index.tsx
  • front_end/src/components/charts/primitives/news_annotations/types.ts
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/timeline/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_info.tsx
  • front_end/src/components/detailed_question_card/detailed_question_card/index.tsx
  • front_end/src/components/charts/primitives/chart_container.tsx
  • front_end/src/components/charts/primitives/news_annotations/timeline_news_annotations.tsx
  • front_end/src/components/charts/primitives/news_annotations/utils.ts
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (3)
front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/key_factor_strength_item.tsx (1)

191-197: Analytics sent before submission may track failed votes.

In submitImpactVote, the analytics event is dispatched before calling submit. If the submission fails, the analytics event will have already been sent. This is inconsistent with the submit function which sends analytics only after success.

Consider moving the analytics call after submit completes, or use try/await to track only successful impact votes:

♻️ Proposed fix for consistent analytics timing
  const submitImpactVote = useCallback(
-    (option: ImpactOption) => {
+    async (option: ImpactOption) => {
       if (!user || user.is_bot) return;
       const currentVote = aggregate?.user_vote;
       if (!currentVote || currentVote <= 0) return;

       const newScore = IMPACT_SCORE_MAP[option];
       if (newScore === currentVote) return;

-      sendAnalyticsEvent("KeyFactorVote", {
-        event_label: newScore.toString(),
-        variant: "impact",
-        impact_level: option,
-      });
-
-      submit(newScore, { skipAnalytics: true });
+      try {
+        await submit(newScore, { skipAnalytics: true });
+        sendAnalyticsEvent("KeyFactorVote", {
+          event_label: newScore.toString(),
+          variant: "impact",
+          impact_level: option,
+        });
+      } catch {
+        // Analytics not sent on failure
+      }
     },
     [user, aggregate?.user_vote, submit]
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/item_view/key_factor_strength_item.tsx
around lines 191 - 197, The analytics event for impact votes is sent before the
network submission, which can record failed votes; in the submitImpactVote flow
call submit(...) first and only after it resolves successfully invoke
sendAnalyticsEvent("KeyFactorVote", { event_label: newScore.toString(), variant:
"impact", impact_level: option }), or use try/await around submit to send
analytics in the success path and avoid sending on errors; update the
submitImpactVote function (referencing submit and sendAnalyticsEvent) so
analytics are emitted after submit completes without errors.
front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/index.tsx (1)

128-133: Analytics sent before API call may track failed downvote attempts.

Similar to submitImpactVote in the sibling file, the analytics event is dispatched before the voteKeyFactor call. If the API call fails (caught at lines 147-149), the downvote analytics will already have been recorded.

For consistency with the submit function in key_factor_strength_item.tsx (which sends analytics only after success), consider moving this after the API response:

♻️ Proposed fix for consistent analytics timing
  const handleDownvoteReasonSelect = useCallback(
    async (reason: DownvoteReason) => {
      if (!user || user.is_bot) return;

      setShowDownvoteThanks(true);
      const apiReason = DOWNVOTE_REASON_TO_API[reason];

-      sendAnalyticsEvent("KeyFactorVote", {
-        event_label: downScore.toString(),
-        variant: "downvote_reason",
-        reason: apiReason,
-      });
-
       try {
         const resp = await voteKeyFactor({
           id: liveKeyFactor.id,
           vote: downScore,
           user: user.id,
           vote_type: downVoteType,
           vote_reason: apiReason,
         });
+
+        sendAnalyticsEvent("KeyFactorVote", {
+          event_label: downScore.toString(),
+          variant: "downvote_reason",
+          reason: apiReason,
+        });
+
         if (resp) {
           const updated = resp as unknown as KeyFactorVoteAggregate;
           commentsFeed?.setKeyFactorVote(liveKeyFactor.id, updated);
         }
       } catch (e) {
         console.error("Failed to submit vote reason", e);
       }
     },
     [user, liveKeyFactor.id, downScore, downVoteType, commentsFeed]
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/item_view/index.tsx
around lines 128 - 133, The analytics event for downvotes is sent before the API
call; move the sendAnalyticsEvent call so it runs only after a successful
voteKeyFactor() response to avoid tracking failed attempts—locate the block
using sendAnalyticsEvent("KeyFactorVote", { event_label: downScore.toString(),
variant: "downvote_reason", reason: apiReason }) and the voteKeyFactor(...) call
in the same handler (the downvote path in this component), call voteKeyFactor
first, await its success, and then invoke sendAnalyticsEvent with the same
payload (retain downScore and apiReason) inside the try/then-success branch;
keep the existing error handling around voteKeyFactor unchanged.
front_end/src/components/charts/primitives/chart_container.tsx (1)

65-101: Redundant uppercase class on line 89.

The uppercase class is already applied unconditionally on line 79, making the conditional application on line 89 (isEmbed && "uppercase ...") redundant for that specific class.

♻️ Suggested cleanup
                             className={cn(
-                               "ChartZoomButton rounded px-1 py-0.5 text-xs font-normal uppercase leading-4 text-gray-600 hover:text-blue-800 focus:outline-none dark:text-gray-600-dark hover:dark:text-blue-800-dark md:text-sm",
+                               "ChartZoomButton rounded px-1 py-0.5 text-xs font-normal uppercase leading-4 text-gray-600 hover:text-blue-800 focus:outline-none dark:text-gray-600-dark hover:dark:text-blue-800-dark md:text-sm",
                               {
                                 "text-gray-900 dark:text-gray-900-dark":
                                   selected,
                               },
                               {
                                 "bg-gray-300 dark:bg-gray-300-dark":
                                   hover || selected,
                               },
-                               isEmbed &&
-                                 "uppercase text-gray-600 dark:text-gray-600-dark md:text-xs"
+                               isEmbed && "md:text-xs"
                             )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front_end/src/components/charts/primitives/chart_container.tsx` around lines
65 - 101, The className on the zoom buttons applies "uppercase" unconditionally
and then again inside the conditional for isEmbed; update the Tab rendering in
the TabGroup/TabList (where tabOptions is mapped) to remove the redundant
"uppercase" from the isEmbed branch so only the unconditional "uppercase"
remains—adjust the conditional expression inside the className computation for
the button in that Tab (used in the mapping over tabOptions) to omit "uppercase"
while keeping the other isEmbed styles intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@front_end/src/components/charts/primitives/news_annotations/timeline_news_annotations.tsx`:
- Around line 32-48: The pending closeTimeout created in handleHoverEnd can fire
after the component unmounts and call setActiveClusterId, so add a useEffect
cleanup that clears closeTimeoutRef.current (if present) and sets it to null on
unmount; reference the existing closeTimeoutRef, handleHoverEnd,
handleHoverStart and setActiveClusterId when locating where to add the cleanup
to prevent state updates on an unmounted component.

---

Nitpick comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/item_view/index.tsx:
- Around line 128-133: The analytics event for downvotes is sent before the API
call; move the sendAnalyticsEvent call so it runs only after a successful
voteKeyFactor() response to avoid tracking failed attempts—locate the block
using sendAnalyticsEvent("KeyFactorVote", { event_label: downScore.toString(),
variant: "downvote_reason", reason: apiReason }) and the voteKeyFactor(...) call
in the same handler (the downvote path in this component), call voteKeyFactor
first, await its success, and then invoke sendAnalyticsEvent with the same
payload (retain downScore and apiReason) inside the try/then-success branch;
keep the existing error handling around voteKeyFactor unchanged.

In
`@front_end/src/app/`(main)/questions/[id]/components/key_factors/item_view/key_factor_strength_item.tsx:
- Around line 191-197: The analytics event for impact votes is sent before the
network submission, which can record failed votes; in the submitImpactVote flow
call submit(...) first and only after it resolves successfully invoke
sendAnalyticsEvent("KeyFactorVote", { event_label: newScore.toString(), variant:
"impact", impact_level: option }), or use try/await around submit to send
analytics in the success path and avoid sending on errors; update the
submitImpactVote function (referencing submit and sendAnalyticsEvent) so
analytics are emitted after submit completes without errors.

In `@front_end/src/components/charts/primitives/chart_container.tsx`:
- Around line 65-101: The className on the zoom buttons applies "uppercase"
unconditionally and then again inside the conditional for isEmbed; update the
Tab rendering in the TabGroup/TabList (where tabOptions is mapped) to remove the
redundant "uppercase" from the isEmbed branch so only the unconditional
"uppercase" remains—adjust the conditional expression inside the className
computation for the button in that Tab (used in the mapping over tabOptions) to
omit "uppercase" while keeping the other isEmbed styles intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28f5f9e9-9142-48f8-9a79-0ce9bc0e78b4

📥 Commits

Reviewing files that changed from the base of the PR and between cdfdc61 and 6acf0f3.

⛔ Files ignored due to path filters (1)
  • front_end/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/package.json
  • front_end/src/app/(main)/questions/[id]/components/key_factors/comment_detail_panel.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/key_factor_strength_item.tsx
  • front_end/src/components/charts/numeric_chart.tsx
  • front_end/src/components/charts/primitives/chart_container.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx
  • front_end/src/components/charts/primitives/news_annotations/timeline_news_annotations.tsx
✅ Files skipped from review due to trivial changes (9)
  • front_end/package.json
  • front_end/messages/en.json
  • front_end/messages/pt.json
  • front_end/messages/zh.json
  • front_end/messages/zh-TW.json
  • front_end/messages/cs.json
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup.tsx
  • front_end/messages/es.json
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_toggle.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_marker.tsx
  • front_end/src/components/charts/primitives/news_annotations/news_annotation_popup_card.tsx

Comment on lines +32 to +48
const [activeClusterId, setActiveClusterId] = useState<number | null>(null);
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const handleHoverStart = useCallback((index: number) => {
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = null;
}
setActiveClusterId(index);
}, []);

const handleHoverEnd = useCallback(() => {
closeTimeoutRef.current = setTimeout(() => {
setActiveClusterId(null);
closeTimeoutRef.current = null;
}, 150);
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear timeout on unmount to prevent state updates on unmounted component.

If the component unmounts while the close timeout is pending, setActiveClusterId(null) will be called on an unmounted component, potentially causing a memory leak or React warning.

🛡️ Proposed fix
+import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";

 const TimelineNewsAnnotations: FC<Props> = ({
   clusters,
   chartHeight,
   axisBottomOffset,
   questionType,
 }) => {
   const [activeClusterId, setActiveClusterId] = useState<number | null>(null);
   const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

+  useEffect(() => {
+    return () => {
+      if (closeTimeoutRef.current) {
+        clearTimeout(closeTimeoutRef.current);
+      }
+    };
+  }, []);
+
   const handleHoverStart = useCallback((index: number) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/components/charts/primitives/news_annotations/timeline_news_annotations.tsx`
around lines 32 - 48, The pending closeTimeout created in handleHoverEnd can
fire after the component unmounts and call setActiveClusterId, so add a
useEffect cleanup that clears closeTimeoutRef.current (if present) and sets it
to null on unmount; reference the existing closeTimeoutRef, handleHoverEnd,
handleHoverStart and setActiveClusterId when locating where to add the cleanup
to prevent state updates on an unmounted component.

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.

2 participants