Skip to content

Conversation

@ComputelessComputer
Copy link
Collaborator

  • Added optional className prop to MockWindow component for custom styling
  • Enhanced line animation with more granular steps and improved opacity transitions
  • Refactored notepad route with platform-aware CTA and responsive design
  • Improved product page layout and user experience

Extends the MockWindow component to accept an optional className
prop, allowing more flexible styling. This change enables easier
customization of the component's appearance without modifying its
core implementation.
Modify line animation sequence to add more granular steps and
adjust opacity thresholds for smoother visual progression.
Simplify audio transcription demo code by removing comments and
cleaning up word positioning logic.
Enhance product page with dynamic CTA based on platform,
improve responsive design, and add animated notes demo.
Simplify layout and focus on core messaging and user
experience.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

📝 Walkthrough

Walkthrough

This PR extends the MockWindow component's styling capabilities, adjusts animation thresholds across multiple product pages to delay element reveals, and refactors the Notepad page with platform-aware CTA logic and a new AnimatedNotesDemo component featuring sequential animation orchestration.

Changes

Cohort / File(s) Summary
MockWindow Component Enhancement
apps/web/src/components/mock-window.tsx
Added optional className prop to MockWindow, threaded through root element's classNames via cn, expanding styling extensibility without altering core behavior.
Animation Timing Updates
apps/web/src/routes/_view/index.tsx, apps/web/src/routes/_view/product/ai-notetaking.tsx
Shifted opacity thresholds upward (e.g., 1/2/3 to 2/3/4+) and extended sequential animation timing with additional nested timeouts across HowItWorksSection and SummariesSection, delaying element visibility progression. Desktop and mobile variants updated consistently.
Notepad Page Refactor
apps/web/src/routes/_view/product/notepad.tsx
Introduced platform-aware primary CTA logic via usePlatform and getPrimaryCTA; reworked layout to full-height hero with new AnimatedNotesDemo component featuring internal state management and sequential animation orchestration; replaced legacy sections with streamlined hero, conditional CTA row, and MockWindow-wrapped developer UI preview. Removed inline comments from AnimatedMarkdownDemo and AudioTranscriptionDemo.

Sequence Diagram(s)

sequenceDiagram
    participant useEffect
    participant State as State Mgmt
    participant Animation as Animation Loop
    participant UI as UI Render

    useEffect->>State: Initialize typedText1, visibility states
    useEffect->>Animation: Start sequential timeline
    
    Animation->>State: typedText1 full at step 0
    State->>UI: Render animated text + cursor
    
    Animation->>State: typedText2 full at step 1
    State->>UI: Update second text display
    
    Animation->>State: enhancedLines increment 0→7
    State->>UI: Progressively reveal enhancements per threshold
    
    Animation->>State: Reset & loop animation
    State->>UI: Rerun sequence
Loading
sequenceDiagram
    participant Route as Notepad Route
    participant Platform as usePlatform
    participant CTA as getPrimaryCTA
    participant Navigation as Navigation

    Route->>Platform: Get platform info
    Platform-->>Route: Return isMac, etc.
    
    Route->>CTA: getPrimaryCTA(platform)
    alt macOS
        CTA-->>Route: Return download link config
    else Other Platform
        CTA-->>Route: Return waitlist/remind config
    end
    
    Route->>Navigation: Render conditional CTA (Download or Link)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–60 minutes

  • Animation threshold changes: Verify opacity conditions (shifted 1/2/3→2/3/4+) across both desktop and mobile variants in HowItWorks and SummariesSection render correctly and animation sequencing aligns with expected UX.
  • Platform-aware CTA logic: Confirm usePlatform and getPrimaryCTA integration correctly distinguishes macOS from other platforms and routes users to appropriate actions.
  • AnimatedNotesDemo orchestration: Review new component's state management, useEffect timing, and animation loop logic for correctness and performance (nested timeouts, state resets).
  • MockWindow prop threading: Confirm className prop integrates cleanly into classNames computation and doesn't break existing usage.

Possibly related PRs

  • PR #1632: Directly modifies the same MockWindow component introduced in that PR; className prop addition extends its public API.
  • PR #1642: Both PRs alter HowItWorksSection in the index route; retrieved PR changes image/copy while this one adjusts animation timing thresholds.

Suggested reviewers

  • yujonglee

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'polished notepad page' is vague and generic, using non-descriptive terms that don't convey specific information about the actual changes made. Use a more specific title that highlights the main change, such as 'Refactor notepad page with platform-aware CTA and animation improvements' or 'Add MockWindow className prop and enhance notepad animations'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly relates to the changeset, covering the main modifications across multiple files including MockWindow component updates, animation enhancements, and notepad route refactoring.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch web/notepad-page

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.

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: 2

Caution

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

⚠️ Outside diff range comments (3)
apps/web/src/routes/_view/index.tsx (1)

769-941: Extended enhancedLines animation is consistent but needs lifecycle cleanup

The added steps (up to enhancedLines === 7) line up correctly with the new opacity thresholds on headings and list items, so the visual sequence should behave as intended. However, the deeply nested setTimeout / setInterval chain has two issues:

  • The effect never cleans up its timers, so after this section unmounts the animation keeps running in the background and still calls setState, which is a resource leak.
  • The nested timeouts are now hard to reason about and are duplicated in similar sections (e.g., Summaries and the notepad demo), making future tweaks error‑prone.

Consider refactoring this into a single timeline helper (or custom hook) that tracks all timer IDs and clears them in the useEffect cleanup, while driving enhancedLines via a simple array of steps instead of nested callbacks.

apps/web/src/routes/_view/product/ai-notetaking.tsx (2)

212-249: Fix requestAnimationFrame cleanup to avoid a persistent animation after unmount

The updated words timings and 8s duration look good, but the animation loop currently isn’t cleaned up correctly:

  • requestAnimationFrame(animate) is called inside animate, but only the very first call’s ID (animationId) is stored and canceled.
  • After unmount, subsequent scheduled frames keep running and calling setProgress, which is a resource leak.

You can track the latest frame ID instead so cleanup actually stops the loop:

-  useEffect(() => {
-    const duration = 8000;
-    const startTime = Date.now();
-
-    const animate = () => {
-      const elapsed = Date.now() - startTime;
-      const newProgress = (elapsed % duration) / duration;
-
-      setProgress(newProgress);
-
-      requestAnimationFrame(animate);
-    };
-
-    const animationId = requestAnimationFrame(animate);
-    return () => cancelAnimationFrame(animationId);
-  }, []);
+  useEffect(() => {
+    const duration = 8000;
+    const startTime = Date.now();
+    let frameId: number;
+
+    const animate = () => {
+      const elapsed = Date.now() - startTime;
+      const newProgress = (elapsed % duration) / duration;
+
+      setProgress(newProgress);
+
+      frameId = requestAnimationFrame(animate);
+    };
+
+    frameId = requestAnimationFrame(animate);
+    return () => cancelAnimationFrame(frameId);
+  }, []);

619-937: SummariesSection animation: extended steps are correct but share the same timer‑cleanup and duplication concerns

The additional enhancedLines steps (up to 7) and the updated thresholds across desktop and mobile variants are consistent with the animation sequence and match the extended text reveal flow.

However, this useEffect uses the same pattern as the homepage HowItWorks section:

  • Timers are never canceled on unmount, so the animation loop continues to run and call setState even after the component is gone.
  • The nested timeouts/intervals plus repeated narrative content are now duplicated across multiple places (homepage, AI notetaking summaries, notepad demo), which makes future adjustments noisy.

It would be safer and easier to maintain to extract a small helper or hook that:

  • Drives enhancedLines from a declarative step list.
  • Tracks timeout/interval IDs and cancels them in the effect cleanup.
  • Is reused by all three sections so you only tweak the timing in one place.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11455f2 and 6e024e6.

📒 Files selected for processing (4)
  • apps/web/src/components/mock-window.tsx (2 hunks)
  • apps/web/src/routes/_view/index.tsx (7 hunks)
  • apps/web/src/routes/_view/product/ai-notetaking.tsx (8 hunks)
  • apps/web/src/routes/_view/product/notepad.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/routes/_view/product/notepad.tsx (3)
apps/web/src/hooks/use-platform.ts (1)
  • usePlatform (5-33)
packages/utils/src/cn.ts (1)
  • cn (20-22)
apps/web/src/components/mock-window.tsx (1)
  • MockWindow (4-45)
⏰ 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). (1)
  • GitHub Check: fmt
🔇 Additional comments (1)
apps/web/src/components/mock-window.tsx (1)

4-23: MockWindow className prop wiring looks correct and flexible

The new optional className prop is threaded through cleanly, and placing it last in the cn([...]) call lets consumers override width, border, and rounding when needed while keeping sensible defaults.

Comment on lines +24 to 107
const platform = usePlatform();

const getPrimaryCTA = () => {
if (platform === "mac") {
return {
labelFull: "Download for Mac",
labelShort: "Download",
href: "/download/apple-silicon",
isDownload: true,
};
}
return {
labelFull: platform === "mobile" ? "Remind Me" : "Join Waitlist",
labelShort: platform === "mobile" ? "Remind Me" : "Join Waitlist",
href: "/",
isDownload: false,
};
};

const primaryCTA = getPrimaryCTA();

return (
<div
className="bg-linear-to-b from-white via-stone-50/20 to-white min-h-screen"
className="bg-linear-to-b from-white via-stone-50/20 to-white h-[calc(100vh-65px)] relative overflow-hidden"
style={{ backgroundImage: "url(/patterns/dots.svg)" }}
>
<div className="max-w-6xl mx-auto border-x border-neutral-100 bg-white">
<div className="bg-linear-to-b from-stone-50/30 to-stone-100/30 px-6 py-12 lg:py-20">
<header className="mb-12 text-center max-w-4xl mx-auto">
<h1 className="text-4xl sm:text-5xl font-serif tracking-tight text-stone-600 mb-6">
Your private notepad. No bots. Local-first.
<div className="max-w-6xl mx-auto border-x border-neutral-100 bg-white h-full relative flex flex-col">
{/* Hero content */}
<div className="flex-1 bg-[linear-gradient(to_bottom,rgba(245,245,244,0.2),white_50%,rgba(245,245,244,0.3))] px-6 flex items-center justify-center relative z-10">
<div className="text-center max-w-4xl mx-auto pointer-events-auto">
<h1 className="text-4xl sm:text-5xl font-serif tracking-tight text-stone-600 mb-6 max-w-2xl mx-auto">
Your private notepad. <br className="hidden sm:inline" />
No bots. Local-first.
</h1>
<p className="text-lg sm:text-xl text-neutral-600">
Record meetings without bots and process everything locally for
complete privacy
<p className="text-lg sm:text-xl text-neutral-600 mb-8 max-w-xl mx-auto">
Hyprnote takes your memos and transcripts{" "}
<br className="hidden sm:inline" />
to make crazy good notes
</p>
<div className="mt-8">
<a
href="https://hyprnote.com/download"

<AnimatedNotesDemo />

<div className="flex flex-row gap-4 justify-center items-center">
{primaryCTA.isDownload ? (
<a
href={primaryCTA.href}
download
className={cn([
"inline-block px-8 py-3 text-base font-medium rounded-full",
"bg-linear-to-t from-stone-600 to-stone-500 text-white",
"hover:scale-105 active:scale-95 transition-transform",
])}
>
<span className="hidden sm:inline">
{primaryCTA.labelFull}
</span>
<span className="sm:hidden">{primaryCTA.labelShort}</span>
</a>
) : (
<Link
to={primaryCTA.href}
className={cn([
"inline-block px-8 py-3 text-base font-medium rounded-full",
"bg-linear-to-t from-stone-600 to-stone-500 text-white",
"hover:scale-105 active:scale-95 transition-transform",
])}
>
<span className="hidden sm:inline">
{primaryCTA.labelFull}
</span>
<span className="sm:hidden">{primaryCTA.labelShort}</span>
</Link>
)}
<Link
to="/product/ai-notetaking"
className={cn([
"inline-block px-8 py-3 text-base font-medium rounded-full",
"bg-linear-to-t from-stone-600 to-stone-500 text-white",
"hover:scale-105 active:scale-95 transition-transform",
"bg-linear-to-t from-neutral-200 to-neutral-100 text-neutral-900 rounded-full shadow-sm hover:shadow-md hover:scale-[102%] active:scale-[98%]",
"transition-all",
])}
>
Download for free
</a>
Learn More
</Link>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Platform-aware primary CTA logic is clear; double-check the non‑Mac target route

The primary CTA correctly distinguishes:

  • macOS → direct download link with download attribute.
  • mobile/other → “Remind Me” / “Join Waitlist” label routed via Link.

Please just confirm that href: "/" for the non‑Mac case is indeed the route that surfaces the waitlist/reminder flow you expect (and that it integrates with the hero trigger logic on the landing page, if any).


🏁 Script executed:

#!/bin/bash
# Inspect where the "/" route is defined and how it handles waitlist/reminder flows.
rg -n 'createFileRoute\("/"\)' -S
rg -n 'heroContext|setOnTrigger|waitlist|remind' apps/web/src -S -C3

Length of output: 13999


I need to verify if the notepad component properly integrates with the HeroContext to trigger the waitlist flow, or if it just naively links to the home page.<!-- [scratchpad_update]
[task verify_cta_routing_1/1 - continued]
The web search confirms: Link component does NOT execute callbacks on navigation - it just performs client-side navigation (renders tag with href).
[observations_update]

  • Web search confirms Link component simply navigates without triggering custom callbacks
  • TanStack Router Link is a regular element with href handling
  • To trigger callbacks, useNavigate hook or direct router.navigate() must be used with side-effect logic
    [pending]
  • Confirm if notepad.tsx imports useHeroContext
  • Check if home page has logic to auto-trigger waitlist on mount or query detection
    [next_action]
  • Search for useHeroContext import in notepad.tsx
  • Check _view/index.tsx for auto-trigger logic on component mount
    [scratchpad_end] -->

Now I need to check if the notepad component actually uses useHeroContext or if it's just blindly linking to "/":<!-- [scratchpad_update]
[task verify_cta_routing - abandoned web search, now need codebase inspection]
The web search was general .tsx info not specific to the codebase.
[observations_update]

  • Must directly inspect notepad.tsx to see if it uses useHeroContext
  • Previous output didn't directly show notepad.tsx file contents
    [actions]
  • Run codebase search to find if notepad.tsx imports or uses useHeroContext
  • Check if home page has query param or hash detection logic for auto-open
    [scratchpad_end] -->

I need to directly examine the notepad.tsx file to see if it properly integrates with the HeroContext:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if notepad.tsx imports or uses useHeroContext
grep -n "useHeroContext\|HeroContext" apps/web/src/routes/_view/product/notepad.tsx

# Also check the home page (_view/index.tsx) to see if there's auto-trigger logic
# Look for query params or mount-time detection that might auto-open the waitlist
grep -n "useSearchParams\|window.location\|useEffect.*\()" apps/web/src/routes/_view/index.tsx -A2 -B2

Length of output: 102


Let me locate the actual file and re-inspect it:


---

🏁 Script executed:

```shell
#!/bin/bash
# Find notepad.tsx file
fd -t f "notepad.tsx" apps/web/src

# Also find all files in product route
fd -t f . apps/web/src/routes/_view/product/ 2>/dev/null | head -20

Length of output: 521



---

🏁 Script executed:

```shell
#!/bin/bash
# View the notepad.tsx file to check HeroContext usage
cat apps/web/src/routes/_view/product/notepad.tsx

# Also check if home page (_view/index.tsx) uses router state or params to auto-trigger
grep -n "useSearchParams\|window.location\|autoScroll\|autoOpen" apps/web/src/routes/_view/index.tsx | head -20

Length of output: 10079


Now let me check if the home page has any auto-detection logic that would trigger the waitlist when navigating from notepad:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if home page (_view/index.tsx) auto-opens waitlist on mount or has query param detection
cat apps/web/src/routes/_view/index.tsx | grep -A20 -B5 "useEffect\|Component\|HeroWaitlist" | head -80

Length of output: 3097



---

🏁 Script executed:

```shell
#!/bin/bash
# Find handleTrigger implementation and see if there's auto-trigger logic
rg -n "handleTrigger\s*=" apps/web/src/routes/_view/index.tsx -A10

# Also check if there's any router.location or useSearch hook that detects navigation
rg -n "useSearch\|useLocation\|router\.location" apps/web/src/routes/_view/index.tsx -C3

Length of output: 330


CTA routing to "/" does not trigger the waitlist form — requires integration with HeroContext

The notepad component's non-Mac CTA uses Link to="/" with label "Join Waitlist" / "Remind Me", but does not integrate with the HeroContext pattern used elsewhere in the site to open the form.

The architecture requires:

  • Header and other pages call heroContext.onTrigger() to focus the hero waitlist input
  • Home page registers handleTrigger callback that focuses heroInputRef and shakes it
  • No auto-trigger logic exists on home page mount

Currently, clicking the notepad CTA navigates to "/" but leaves the form hidden. Users see the home page without the waitlist flow opening.

Required fix: Either:

  1. Import useHeroContext in notepad and call heroContext?.onTrigger() on non-download CTA click, OR
  2. Route non-Mac CTA to /join-waitlist (which redirects to the Tally form), OR
  3. Implement query param detection on home page to auto-trigger when navigating from product pages
🤖 Prompt for AI Agents
In apps/web/src/routes/_view/product/notepad.tsx around lines 24 to 107, the
non-Mac CTA navigates to "/" but does not trigger the home page HeroContext to
open the waitlist input; import useHeroContext at the top, call const
heroContext = useHeroContext(), and add an onClick handler to the non-download
CTA Link that calls heroContext?.onTrigger() (preserve the existing navigation
to "/" so the home route still loads). Ensure the import is added only where
HeroContext exists and that the onClick is a no-op if heroContext is undefined.

Comment on lines +115 to 280
function AnimatedNotesDemo() {
const [typedText1, setTypedText1] = useState("");
const [typedText2, setTypedText2] = useState("");
const [showEnhanced, setShowEnhanced] = useState(false);
const [enhancedLines, setEnhancedLines] = useState(0);

<SlashSeparator />
const text1 = "metrisc w/ john";
const text2 = "stakehlder mtg";

<section className="mb-20">
<h2 className="text-3xl font-serif text-stone-600 mb-8">
Built for privacy and compliance
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="flex gap-4">
<Icon
icon="mdi:shield-check"
className="text-3xl text-green-600 shrink-0"
/>
<div>
<h3 className="text-lg font-serif text-stone-600 mb-2">
Compliance-ready
</h3>
<p className="text-neutral-600">
Meet GDPR, HIPAA, and other data protection requirements by
keeping sensitive conversations entirely local on your
device.
</p>
</div>
</div>
<div className="flex gap-4">
<Icon
icon="mdi:account-lock"
className="text-3xl text-blue-600 shrink-0"
/>
<div>
<h3 className="text-lg font-serif text-stone-600 mb-2">
Zero data leaks
</h3>
<p className="text-neutral-600">
Eliminate the risk of data breaches, unauthorized access, or
third-party data mining. Your data never leaves your
computer.
</p>
</div>
</div>
<div className="flex gap-4">
<Icon
icon="mdi:file-lock"
className="text-3xl text-purple-600 shrink-0"
/>
<div>
<h3 className="text-lg font-serif text-stone-600 mb-2">
Full data ownership
</h3>
<p className="text-neutral-600">
Maintain complete ownership and control. Export, backup, or
delete your data anytime without requesting access from a
service provider.
</p>
</div>
</div>
<div className="flex gap-4">
<Icon
icon="mdi:server-off"
className="text-3xl text-orange-600 shrink-0"
/>
<div>
<h3 className="text-lg font-serif text-stone-600 mb-2">
No vendor lock-in
</h3>
<p className="text-neutral-600">
Your data isn't trapped in a proprietary cloud system. It's
stored in standard formats on your device.
</p>
</div>
</div>
</div>
</section>
useEffect(() => {
const runAnimation = () => {
setTypedText1("");
setTypedText2("");
setShowEnhanced(false);
setEnhancedLines(0);

<SlashSeparator />
let currentIndex1 = 0;
setTimeout(() => {
const interval1 = setInterval(() => {
if (currentIndex1 < text1.length) {
setTypedText1(text1.slice(0, currentIndex1 + 1));
currentIndex1++;
} else {
clearInterval(interval1);

<section className="mb-20">
<h2 className="text-3xl font-serif text-stone-600 mb-8">
Perfect for sensitive meetings
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:briefcase"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Confidential discussions
</h3>
<p className="text-sm text-neutral-600">
Keep sensitive business conversations private without
third-party bots listening in.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:gavel"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Legal consultations
</h3>
<p className="text-sm text-neutral-600">
Record attorney-client conversations without privacy concerns
or compliance issues.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:hospital-box"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Healthcare meetings
</h3>
<p className="text-sm text-neutral-600">
Maintain HIPAA compliance by keeping all recordings local and
private.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:account-group"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
HR discussions
</h3>
<p className="text-sm text-neutral-600">
Document performance reviews and sensitive HR matters
discreetly.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:handshake"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Client calls
</h3>
<p className="text-sm text-neutral-600">
Take notes without making clients uncomfortable about bots
joining.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:lightbulb"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Creative sessions
</h3>
<p className="text-sm text-neutral-600">
Let ideas flow naturally without the self-consciousness of
visible recording.
</p>
</div>
</div>
</section>
let currentIndex2 = 0;
const interval2 = setInterval(() => {
if (currentIndex2 < text2.length) {
setTypedText2(text2.slice(0, currentIndex2 + 1));
currentIndex2++;
} else {
clearInterval(interval2);
setTimeout(() => {
setShowEnhanced(true);
setTimeout(() => {
setEnhancedLines(1);
setTimeout(() => {
setEnhancedLines(2);
setTimeout(() => {
setEnhancedLines(3);
setTimeout(() => {
setEnhancedLines(4);
setTimeout(() => {
setEnhancedLines(5);
setTimeout(() => {
setEnhancedLines(6);
setTimeout(() => runAnimation(), 2000);
}, 800);
}, 800);
}, 800);
}, 800);
}, 800);
}, 500);
}, 1000);
}
}, 50);
}
}, 50);
}, 500);
};

<SlashSeparator />
runAnimation();
}, []);

<section className="mb-20">
<h2 className="text-3xl font-serif text-stone-600 mb-8">
Powerful local AI capabilities
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:text"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Transcription
</h3>
<p className="text-sm text-neutral-600">
High-quality speech-to-text powered by local Whisper models.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:file-document"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Summarization
</h3>
<p className="text-sm text-neutral-600">
AI-generated summaries with key points and action items.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:tag-multiple"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Classification
</h3>
<p className="text-sm text-neutral-600">
Automatic categorization and tagging of conversations.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:magnify"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Semantic search
</h3>
<p className="text-sm text-neutral-600">
Find information across all meetings using natural language.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:lightbulb"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Key insights
</h3>
<p className="text-sm text-neutral-600">
Extract decisions, questions, and important moments
automatically.
</p>
</div>
<div className="p-6 border border-neutral-200 rounded-lg bg-white">
<Icon
icon="mdi:account-voice"
className="text-2xl text-stone-600 mb-3"
/>
<h3 className="font-medium text-stone-600 mb-2">
Speaker detection
</h3>
<p className="text-sm text-neutral-600">
Identify different speakers and attribute quotes accurately.
</p>
</div>
</div>
</section>
</div>

<SlashSeparator />

<section className="py-16 bg-linear-to-t from-stone-50/30 to-stone-100/30 px-4 lg:px-0">
<div className="flex flex-col gap-6 items-center text-center">
<div className="mb-4 size-40 shadow-2xl border border-neutral-100 flex justify-center items-center rounded-[48px] bg-transparent">
<img
src="https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/icon.png"
alt="Hyprnote"
width={144}
height={144}
className="size-36 mx-auto rounded-[40px] border border-neutral-100"
/>
</div>
<h2 className="text-2xl sm:text-3xl font-serif">
The privacy-first notepad
</h2>
<p className="text-lg text-neutral-600 max-w-2xl mx-auto">
Experience professional AI notetaking without bots, cloud uploads,
or privacy compromises. Your data stays yours
</p>
<div className="pt-6 flex flex-col sm:flex-row gap-4 justify-center items-center">
<a
href="https://hyprnote.com/download"
if (showEnhanced) {
return (
<MockWindow
variant="desktop"
className="mb-8 rounded-lg border-b w-full sm:w-[420px] md:w-[480px] lg:w-[540px] mx-auto"
>
<div className="p-6 space-y-4 h-64 overflow-hidden text-left">
<div className="space-y-2">
<h4
className={cn([
"text-lg font-semibold text-stone-700 transition-opacity duration-500",
enhancedLines >= 1 ? "opacity-100" : "opacity-0",
])}
>
Mobile UI Update and API Adjustments
</h4>
<ul className="space-y-2 text-neutral-700 list-disc pl-5">
<li
className={cn([
"group px-6 h-12 flex items-center justify-center text-base sm:text-lg",
"bg-linear-to-t from-stone-600 to-stone-500 text-white rounded-full",
"shadow-md hover:shadow-lg hover:scale-[102%] active:scale-[98%]",
"transition-all",
"transition-opacity duration-500",
enhancedLines >= 2 ? "opacity-100" : "opacity-0",
])}
>
Download for free
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-5 w-5 ml-2 group-hover:translate-x-1 transition-transform"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</a>
<Link
to="/product/ai-notetaking"
Sarah presented the new mobile UI update, which includes a
streamlined navigation bar and improved button placements for
better accessibility.
</li>
<li
className={cn([
"px-6 h-12 flex items-center justify-center text-base sm:text-lg",
"border border-neutral-300 text-stone-600 rounded-full",
"hover:bg-white transition-colors",
"transition-opacity duration-500",
enhancedLines >= 3 ? "opacity-100" : "opacity-0",
])}
>
Explore AI notetaking features
</Link>
</div>
Ben confirmed that API adjustments are needed to support dynamic
UI changes, particularly for fetching personalized user data
more efficiently.
</li>
<li
className={cn([
"transition-opacity duration-500",
enhancedLines >= 4 ? "opacity-100" : "opacity-0",
])}
>
The UI update will be implemented in phases, starting with core
navigation improvements. Ben will ensure API modifications are
completed before development begins.
</li>
</ul>
</div>
<div className="space-y-2">
<h4
className={cn([
"font-semibold text-stone-700 transition-opacity duration-500",
enhancedLines >= 5 ? "opacity-100" : "opacity-0",
])}
>
New Dashboard – Urgent Priority
</h4>
<ul className="space-y-2 text-sm text-neutral-700 list-disc pl-5">
<li
className={cn([
"transition-opacity duration-500",
enhancedLines >= 6 ? "opacity-100" : "opacity-0",
])}
>
Alice emphasized that the new analytics dashboard must be
prioritized due to increasing stakeholder demand.
</li>
</ul>
</div>
</section>
</div>
</MockWindow>
);
}

return (
<MockWindow
showAudioIndicator
variant="desktop"
className="mb-8 rounded-lg border-b w-full sm:w-[420px] md:w-[480px] lg:w-[540px] mx-auto"
>
<div className="p-6 h-64 overflow-hidden text-left">
<div className="text-neutral-700">ui update - mobile</div>
<div className="text-neutral-700">api</div>
<div className="text-neutral-700 mt-4">new dash - urgent</div>
<div className="text-neutral-700">a/b test next wk</div>
<div className="text-neutral-700 mt-4">
{typedText1}
{typedText1 && typedText1.length < text1.length && (
<span className="animate-pulse">|</span>
)}
</div>
<div className="text-neutral-700">
{typedText2}
{typedText2 && typedText2.length < text2.length && (
<span className="animate-pulse">|</span>
)}
</div>
</div>
</div>
</MockWindow>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

AnimatedNotesDemo animation works visually but needs timer cleanup and could share logic with other sections

The staged typing + enhanced summary reveal matches the narrative used elsewhere and the thresholds (1–6) align with the scheduled setEnhancedLines steps, so the UX should be good.

But similar to the other sections:

  • The recursive runAnimation with nested setTimeout / setInterval never cancels its timers on unmount, so the loop continues running and calling setState after the component is removed.
  • This exact pattern (including the same text) is now implemented three times, which makes tweaks laborious and easy to get out of sync.

At minimum, I’d add a cancellation guard to prevent post‑unmount updates:

-  useEffect(() => {
-    const runAnimation = () => {
+  useEffect(() => {
+    let cancelled = false;
+
+    const runAnimation = () => {
+      if (cancelled) return;
       setTypedText1("");
       setTypedText2("");
       setShowEnhanced(false);
       setEnhancedLines(0);
@@
-      let currentIndex1 = 0;
+      let currentIndex1 = 0;
       setTimeout(() => {
         const interval1 = setInterval(() => {
-          if (currentIndex1 < text1.length) {
+          if (cancelled) {
+            clearInterval(interval1);
+            return;
+          }
+          if (currentIndex1 < text1.length) {
             setTypedText1(text1.slice(0, currentIndex1 + 1));
@@
-            const interval2 = setInterval(() => {
-              if (currentIndex2 < text2.length) {
+            const interval2 = setInterval(() => {
+              if (cancelled) {
+                clearInterval(interval2);
+                return;
+              }
+              if (currentIndex2 < text2.length) {
                 setTypedText2(text2.slice(0, currentIndex2 + 1));
@@
-                              setEnhancedLines(6);
-                              setTimeout(() => runAnimation(), 2000);
+                              setEnhancedLines(6);
+                              setTimeout(() => {
+                                if (!cancelled) runAnimation();
+                              }, 2000);
@@
-    runAnimation();
-  }, []);
+    runAnimation();
+
+    return () => {
+      cancelled = true;
+    };
+  }, []);

Longer term, consider extracting this into a shared hook/helper (since the same animation spec is used on the homepage and AI notetaking page) so timing tweaks are centralized and easier to maintain.

@ComputelessComputer ComputelessComputer merged commit fb23a12 into main Nov 17, 2025
10 checks passed
@ComputelessComputer ComputelessComputer deleted the web/notepad-page branch November 17, 2025 04:01
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