-
Notifications
You must be signed in to change notification settings - Fork 434
polished notepad page #1675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
polished notepad page #1675
Conversation
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.
📝 WalkthroughWalkthroughThis 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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45–60 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 cleanupThe 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 nestedsetTimeout/setIntervalchain 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
useEffectcleanup, while drivingenhancedLinesvia 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 unmountThe updated
wordstimings and 8s duration look good, but the animation loop currently isn’t cleaned up correctly:
requestAnimationFrame(animate)is called insideanimate, 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 concernsThe additional
enhancedLinessteps (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
useEffectuses the same pattern as the homepage HowItWorks section:
- Timers are never canceled on unmount, so the animation loop continues to run and call
setStateeven 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
enhancedLinesfrom 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
📒 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 flexibleThe new optional
classNameprop is threaded through cleanly, and placing it last in thecn([...])call lets consumers override width, border, and rounding when needed while keeping sensible defaults.
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
downloadattribute. - 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 -C3Length 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
handleTriggercallback that focusesheroInputRefand 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:
- Import
useHeroContextin notepad and callheroContext?.onTrigger()on non-download CTA click, OR - Route non-Mac CTA to
/join-waitlist(which redirects to the Tally form), OR - 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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
runAnimationwith nestedsetTimeout/setIntervalnever cancels its timers on unmount, so the loop continues running and callingsetStateafter 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.
classNameprop toMockWindowcomponent for custom styling