Skip to content

feat: add a no-ai toggle to my feed#5832

Merged
nimrodkra merged 14 commits intomainfrom
feat/no-ai-feed-toggle
Apr 3, 2026
Merged

feat: add a no-ai toggle to my feed#5832
nimrodkra merged 14 commits intomainfrom
feat/no-ai-feed-toggle

Conversation

@nimrodkra
Copy link
Copy Markdown
Member

@nimrodkra nimrodkra commented Apr 1, 2026

Summary

  • add a GrowthBook-gated No AI mode control for My Feed with the final header UI, tooltip copy, and Cloudinary artwork
  • save the preference to account settings, expose it in Feed settings -> Content preferences, and support hiding the homepage toggle after applying it
  • replace the first-run modal with a lighter toast CTA and polish the copy/styling based on review feedback

Test plan

  • pnpm --filter @dailydotdev/shared lint
  • node ./scripts/typecheck-strict-changed.js
  • NODE_ENV=test pnpm --filter @dailydotdev/shared test -- --runTestsByPath src/components/layout/common.spec.tsx src/hooks/useNoAiFeed.spec.ts

Notes

  • feature remains gated behind no_ai_feed
  • saved preference is available from Feed settings -> Content preferences
  • header toggle is desktop-only

Made with Cursor

Preview domain

https://feat-no-ai-feed-toggle.preview.app.daily.dev

Give users an experiment-gated My Feed toggle that applies the backend no-ai filter without changing their saved feed settings.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Apr 3, 2026 5:54pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Apr 3, 2026 5:54pm

Request Review

@nimrodkra
Copy link
Copy Markdown
Member Author

@claude please review this pr

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 1, 2026

Claude finished @nimrodkra's task in 3m 5s —— View job


PR Review: feat/no-ai-feed-toggle

  • Gather context
  • Read changed files
  • Analyze for bugs, issues, and best practices
  • Post review feedback

Overall the implementation is clean and well-structured. A few issues worth addressing before merging:

Issues

1. FEED_QUERY_WITH_NO_AI is a redundant duplicate of FEED_QUERY

FEED_QUERY was already modified to accept $noAi: Boolean and pass noAi: $noAi. The newly added FEED_QUERY_WITH_NO_AI is structurally identical — the only difference is the GraphQL operation name (FeedWithNoAi vs Feed).

React Query caches by query key (which includes variables), not the GraphQL operation name. So having a different operation name provides zero caching benefit. The two queries are interchangeable, and FEED_QUERY_WITH_NO_AI can be deleted.

packages/shared/src/graphql/feed.ts lines 158–185 — the entire FEED_QUERY_WITH_NO_AI export can be removed. Both places in MainFeedLayout.tsx that use it should revert to FEED_QUERY. Fix this →

2. propsByFeed['my-feed'].queryIfLogged change is dead code

packages/shared/src/components/MainFeedLayout.tsx line 121:

'my-feed': {
  query: ANONYMOUS_FEED_QUERY,
  queryIfLogged: FEED_QUERY_WITH_NO_AI,  // ← never used
},

Inside the useMemo, dynamicPropsByFeed[SharedFeedPage.MyFeed] always sets queryIfLogged to myFeedLoggedInQuery, and dynamicFeedConfig?.queryIfLogged takes precedence (line 368):

dynamicFeedConfig?.queryIfLogged || feedConfig.queryIfLogged || null,

So the propsByFeed assignment is overridden 100% of the time. Reverting this line back to FEED_QUERY (or leaving it as-is once FEED_QUERY_WITH_NO_AI is removed) avoids confusion about what the static config actually does. Fix this →

3. No analytics event on toggle

There's no logEvent call in useNoAiFeed or in the SearchControlHeader when the No AI toggle is activated/deactivated. For a GrowthBook experiment, measuring toggle interactions is essential. Consider calling useLogContext and firing a LogEvent when toggleNoAi is called, similar to how ToggleClickbaitShield logs events.

Minor Issues

4. Unnecessary async wrapper in onClick

packages/shared/src/components/layout/common.tsx lines 203–205:

onClick={async () => {
  await noAiState.onToggle();
}}

Since you're not doing anything after the await, this can be simplified to:

onClick={noAiState.onToggle}

5. Mutable let for button variant

packages/shared/src/components/layout/common.tsx lines 162–166:

let noAiButtonVariant = isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary;
if (noAiState?.isEnabled) {
  noAiButtonVariant = ButtonVariant.Primary;
}

Could be a single expression:

const noAiButtonVariant = noAiState?.isEnabled
  ? ButtonVariant.Primary
  : isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary;

What Looks Good

  • useNoAiFeed is clean: isLoaded logic correctly gates on both feature flag and IDB fetch
  • shouldEvaluateNoAi correctly limits GrowthBook evaluation to My Feed only (not custom default feeds)
  • usePersistentContext types confirmed — setStoredValue is mutateAsync, so toggleNoAi: () => Promise<void> is correctly typed
  • The pressed prop on the Button for accessibility is the right pattern
  • Blocking render with if (shouldEvaluateNoAi && !isNoAiLoaded) return null is consistent with the component's existing loading guard pattern
  • Feature flag featureNoAiFeed = new Feature('no_ai_feed', false) — safe default of false

Replace the header button with a right-aligned No AI switch card, remove the redundant query variant, and add analytics so the experiment is clearer and more measurable.

Made-with: Cursor
@nimrodkra
Copy link
Copy Markdown
Member Author

Addressed the review and tightened the UX on this branch:

Review items addressed:

  • removed the redundant FEED_QUERY_WITH_NO_AI duplicate and reverted My Feed back to FEED_QUERY
  • removed the dead propsByFeed['my-feed'].queryIfLogged override path
  • added toggle analytics via LogEvent.ToggleNoAiFeed

UX updates:

  • replaced the header button with a real switch
  • moved the No AI control into its own right-aligned card so it stands apart from the existing left-side feed controls
  • updated the copy to a clearer / more playful "No AI mode"
  • added helper tooltip copy explaining the filter behavior and that it is intentionally aggressive
  • incorporated the supplied anti-bot image into the control using existing shared primitives (LazyImage, Switch, Tooltip, Typography)

Verification:

  • pnpm --filter @dailydotdev/shared lint
  • node ./scripts/typecheck-strict-changed.js
  • NODE_ENV=test pnpm --filter @dailydotdev/shared test -- --runTestsByPath src/components/layout/common.spec.tsx

Keep the switch aligned with the surrounding feed actions while making the enabled state feel lighter and less visually heavy.

Made-with: Cursor
Match the No AI toggle to the feed control sizing and keep the install extension CTA as the rightmost action in the header.

Made-with: Cursor
Match the No AI label to the shared feed control colors and give the tooltip warmer copy without bringing back the heavier card treatment.

Made-with: Cursor
Match the No AI label to the same lighter theme-aware text tone used by the surrounding feed controls in light mode.

Made-with: Cursor
@nimrodkra
Copy link
Copy Markdown
Member Author

@claude run another code review after all the latest changes I've made here

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 2, 2026

Claude finished @nimrodkra's task in 1m 51s —— View job


PR Review: feat/no-ai-feed-toggle (round 2)

  • Gather context
  • Read changed files
  • Analyze for bugs, issues, and best practices
  • Post review feedback

All issues from the first review have been addressed. The refactor from a button to a Switch card is clean, analytics are wired correctly, and the redundant query duplicate is gone. A few minor items worth a pass:

Issues

1. Dead Origin.CustomFeed branch in analytics

packages/shared/src/components/layout/common.tsx lines 186–191:

extra: JSON.stringify({
  origin:
    feedName === SharedFeedPage.Custom ? Origin.CustomFeed : Origin.Feed,
}),

noAiState is only passed from MainFeedLayout when shouldEvaluateNoAi is true, which requires feedName === SharedFeedPage.MyFeed && !isCustomDefaultFeed. The toggle never renders on Custom feeds, so Origin.CustomFeed is unreachable. The conditional can be replaced with just Origin.Feed. Fix this →

2. Unreachable guard in handleNoAiToggle

packages/shared/src/components/layout/common.tsx lines 178–181:

const handleNoAiToggle = async () => {
  if (!noAiState) {
    return;
  }

handleNoAiToggle is only used inside noAiState?.isAvailable && (...), so noAiState is always defined when this fires. The early-return guard is dead code. Fix this →

Minor Notes

3. Missing test for isAvailable: false

common.spec.tsx covers: toggle renders when isAvailable: true, logs correctly on click. There's no test asserting the control is absent when isAvailable: false. Low risk since noAiState?.isAvailable && is straightforward, but a one-liner assertion (expect(screen.queryByText('No AI mode')).not.toBeInTheDocument()) would add confidence.

4. shouldEvaluate default in useNoAiFeed

packages/shared/src/hooks/useNoAiFeed.ts line 20: shouldEvaluate = true. If anyone calls useNoAiFeed() bare (without the prop), it evaluates the GrowthBook flag and reads from IndexedDB unconditionally. The hook is only called from MainFeedLayout today, but a defensive default of false would match the CLAUDE.md principle of only evaluating flags when the component is in scope. Low priority.

What Looks Good

  • isLoaded logic is solid: correctly short-circuits IDB wait when the feature flag is off
  • shouldEvaluateNoAi = feedName === SharedFeedPage.MyFeed && !isCustomDefaultFeed properly excludes custom default feeds from the flag evaluation
  • noAi: true is only spread into variables when actually active — no spurious noAi: false sent to the API
  • Query key includes noAi via Object.values(variables) — TanStack Query will properly re-fetch on toggle
  • Switch with checked + aria-label is the correct accessibility pattern
  • Image committed to both webapp/public/assets/ and extension/public/assets/ for cross-platform coverage
  • Test uses waitFor to assert async log call after toggle — correct pattern

Remove dead No AI toggle branches and add a regression test for the unavailable state so the header behavior stays explicit.

Made-with: Cursor
Comment thread packages/extension/public/assets/no-ai-feed-toggle.png Outdated
Comment thread packages/shared/src/components/MainFeedLayout.tsx Outdated
Add a persisted No AI preference in content settings and let the header switch prompt, toast, and dismiss natively for discovery.

Made-with: Cursor
Replace the first-run no-ai modal with a lighter toast CTA, move the saved setting higher in Content preferences, and drop the local toggle image asset while tightening the related PR cleanup.

Made-with: Cursor
@nimrodkra
Copy link
Copy Markdown
Member Author

Addressed the latest follow-up here: removed the redundant My Feed query override, replaced the first-run modal with a lighter toast CTA, moved the saved No AI setting higher in Content preferences, and simplified the header control so it no longer depends on the local toggle image asset.

Use the provided Cloudinary asset for the No AI toggle image and keep the header control wired through the shared image constants.

Made-with: Cursor
Clarify that the header toggle can be hidden and later changed again from Feed settings, with a more explicit CTA label.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@capJavert capJavert left a comment

Choose a reason for hiding this comment

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

Just image link again

Comment thread packages/shared/src/lib/image.ts Outdated
Use Cloudinary auto format and quality so the toggle artwork is served in an optimized asset format across devices.

Made-with: Cursor
Use the signed media.daily.dev asset URL so the no-ai toggle artwork loads correctly in preview and production.

Made-with: Cursor
@nimrodkra nimrodkra merged commit cd0770b into main Apr 3, 2026
13 checks passed
@nimrodkra nimrodkra deleted the feat/no-ai-feed-toggle branch April 3, 2026 18:08
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