From abb69b938f38c49d61f41fbd4868310476a501bb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 02:24:25 +0000 Subject: [PATCH 1/2] feat(web): add progress bar with auto-cycling and scroll-into-view for showcase sections - Add useAutoProgress hook for auto-cycling through items every 5 seconds - Add ProgressBar component with visual progress fill animation - Implement progress bar for FloatingPanelSection in ai-notetaking.tsx (mobile, tablet, desktop) - Implement progress bar for MainFeaturesSection in index.tsx (mobile carousel) - Implement progress bar for DetailsSection in index.tsx (mobile, tablet, desktop) - Add scroll-into-view functionality when showcasing elements - Add pause on hover/touch and resume on mouse leave/touch end Co-Authored-By: john@hyprnote.com --- apps/web/src/routes/_view/index.tsx | 456 ++++++++++++++---- .../routes/_view/product/ai-notetaking.tsx | 297 ++++++++++-- 2 files changed, 611 insertions(+), 142 deletions(-) diff --git a/apps/web/src/routes/_view/index.tsx b/apps/web/src/routes/_view/index.tsx index 904dc9a2e5..6ae0163920 100644 --- a/apps/web/src/routes/_view/index.tsx +++ b/apps/web/src/routes/_view/index.tsx @@ -25,6 +25,113 @@ import { useHeroContext } from "./route"; const MUX_PLAYBACK_ID = "bpcBHf4Qv5FbhwWD02zyFDb24EBuEuTPHKFUrZEktULQ"; +const PROGRESS_DURATION = 5000; + +function useAutoProgress({ + itemCount, + selectedIndex, + onSelect, + duration = PROGRESS_DURATION, +}: { + itemCount: number; + selectedIndex: number; + onSelect: (index: number) => void; + duration?: number; +}) { + const [progress, setProgress] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const startTimeRef = useRef(Date.now()); + const pausedProgressRef = useRef(0); + + const pause = useCallback(() => { + pausedProgressRef.current = progress; + setIsPaused(true); + }, [progress]); + + const resume = useCallback(() => { + startTimeRef.current = Date.now() - pausedProgressRef.current * duration; + setIsPaused(false); + }, [duration]); + + const goToIndex = useCallback( + (index: number) => { + onSelect(index); + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + }, + [onSelect], + ); + + useEffect(() => { + if (isPaused) return; + + const animate = () => { + const elapsed = Date.now() - startTimeRef.current; + const newProgress = Math.min(elapsed / duration, 1); + setProgress(newProgress); + + if (newProgress >= 1) { + const nextIndex = (selectedIndex + 1) % itemCount; + onSelect(nextIndex); + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + } + }; + + const intervalId = setInterval(animate, 50); + return () => clearInterval(intervalId); + }, [isPaused, selectedIndex, itemCount, onSelect, duration]); + + useEffect(() => { + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + }, [selectedIndex]); + + return { progress, isPaused, pause, resume, goToIndex }; +} + +function ProgressBar({ + itemCount, + selectedIndex, + progress, + onSelect, + className, +}: { + itemCount: number; + selectedIndex: number; + progress: number; + onSelect: (index: number) => void; + className?: string; +}) { + return ( +
+ {Array.from({ length: itemCount }).map((_, index) => ( + + ))} +
+ ); +} + const mainFeatures = [ { icon: "mdi:text-box-outline", @@ -114,23 +221,49 @@ function Component() { const featuresScrollRef = useRef(null); const heroInputRef = useRef(null); - const scrollToDetail = (index: number) => { + const scrollToDetail = useCallback((index: number) => { setSelectedDetail(index); if (detailsScrollRef.current) { const container = detailsScrollRef.current; const scrollLeft = container.offsetWidth * index; container.scrollTo({ left: scrollLeft, behavior: "smooth" }); } - }; + }, []); - const scrollToFeature = (index: number) => { + const scrollToFeature = useCallback((index: number) => { setSelectedFeature(index); if (featuresScrollRef.current) { const container = featuresScrollRef.current; const scrollLeft = container.offsetWidth * index; container.scrollTo({ left: scrollLeft, behavior: "smooth" }); } - }; + }, []); + + const featuresProgress = useAutoProgress({ + itemCount: mainFeatures.length, + selectedIndex: selectedFeature, + onSelect: scrollToFeature, + }); + + const detailsProgress = useAutoProgress({ + itemCount: detailsFeatures.length, + selectedIndex: selectedDetail, + onSelect: scrollToDetail, + }); + + const handleFeatureSelect = useCallback( + (index: number) => { + featuresProgress.goToIndex(index); + }, + [featuresProgress.goToIndex], + ); + + const handleDetailSelect = useCallback( + (index: number) => { + detailsProgress.goToIndex(index); + }, + [detailsProgress.goToIndex], + ); return (
@@ -1074,12 +1213,18 @@ export function MainFeaturesSection({ featuresScrollRef, selectedFeature, setSelectedFeature, - scrollToFeature, + progress, + onSelect, + onPause, + onResume, }: { featuresScrollRef: React.RefObject; selectedFeature: number; setSelectedFeature: (index: number) => void; - scrollToFeature: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { return (
@@ -1106,7 +1251,10 @@ export function MainFeaturesSection({ featuresScrollRef={featuresScrollRef} selectedFeature={selectedFeature} setSelectedFeature={setSelectedFeature} - scrollToFeature={scrollToFeature} + progress={progress} + onSelect={onSelect} + onPause={onPause} + onResume={onResume} />
@@ -1117,15 +1265,25 @@ function FeaturesMobileCarousel({ featuresScrollRef, selectedFeature, setSelectedFeature, - scrollToFeature, + progress, + onSelect, + onPause, + onResume, }: { featuresScrollRef: React.RefObject; selectedFeature: number; setSelectedFeature: (index: number) => void; - scrollToFeature: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { return ( -
+
-
- {mainFeatures.map((_, index) => ( -
+
); } @@ -1285,12 +1435,18 @@ export function DetailsSection({ detailsScrollRef, selectedDetail, setSelectedDetail, - scrollToDetail, + progress, + onSelect, + onPause, + onResume, }: { detailsScrollRef: React.RefObject; selectedDetail: number; setSelectedDetail: (index: number) => void; - scrollToDetail: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { return (
@@ -1299,13 +1455,25 @@ export function DetailsSection({ detailsScrollRef={detailsScrollRef} selectedDetail={selectedDetail} setSelectedDetail={setSelectedDetail} - scrollToDetail={scrollToDetail} + progress={progress} + onSelect={onSelect} + onPause={onPause} + onResume={onResume} /> + -
); } @@ -1328,15 +1496,25 @@ function DetailsMobileCarousel({ detailsScrollRef, selectedDetail, setSelectedDetail, - scrollToDetail, + progress, + onSelect, + onPause, + onResume, }: { detailsScrollRef: React.RefObject; selectedDetail: number; setSelectedDetail: (index: number) => void; - scrollToDetail: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { return ( -
+
-
- {detailsFeatures.map((_, index) => ( -
+
); } function DetailsTabletView({ selectedDetail, - setSelectedDetail, + progress, + onSelect, + onPause, + onResume, }: { selectedDetail: number; - setSelectedDetail: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { + const tabsContainerRef = useRef(null); + + useEffect(() => { + if (tabsContainerRef.current) { + const container = tabsContainerRef.current; + const selectedButton = container.children[0]?.children[ + selectedDetail + ] as HTMLElement; + if (selectedButton) { + selectedButton.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + } + }, [selectedDetail]); + return ( -
+
-
+
{detailsFeatures.map((feature, index) => (
+ +
); } -function DetailsDesktopView() { - const [selectedDetail, setSelectedDetail] = useState(0); +function DetailsDesktopView({ + selectedDetail, + progress, + onSelect, + onPause, + onResume, +}: { + selectedDetail: number; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; +}) { const [hoveredDetail, setHoveredDetail] = useState(null); + const tabsContainerRef = useRef(null); const selectedFeature = selectedDetail !== null ? detailsFeatures[selectedDetail] : null; + useEffect(() => { + if (tabsContainerRef.current) { + const selectedElement = tabsContainerRef.current.children[ + selectedDetail + ] as HTMLElement; + if (selectedElement) { + selectedElement.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + } + } + }, [selectedDetail]); + return ( -
+
-
+
{detailsFeatures.map((feature, index) => (
setSelectedDetail(index)} + onClick={() => onSelect(index)} className={cn([ "p-6 cursor-pointer transition-colors", index < detailsFeatures.length - 1 && @@ -1522,51 +1764,61 @@ function DetailsDesktopView() {
-
setHoveredDetail(selectedDetail)} - onMouseLeave={() => setHoveredDetail(null)} - > - {selectedFeature && - (selectedFeature.image ? ( - <> - {`${selectedFeature.title} - {selectedFeature.link && ( -
- +
setHoveredDetail(selectedDetail)} + onMouseLeave={() => setHoveredDetail(null)} + > + {selectedFeature && + (selectedFeature.image ? ( + <> + {`${selectedFeature.title} + {selectedFeature.link && ( +
- Learn more - -
- )} - - ) : ( - {`${selectedFeature.title} - ))} + + Learn more + +
+ )} + + ) : ( + {`${selectedFeature.title} + ))} +
+ +
); diff --git a/apps/web/src/routes/_view/product/ai-notetaking.tsx b/apps/web/src/routes/_view/product/ai-notetaking.tsx index a82aa0a4a7..9b20cb9377 100644 --- a/apps/web/src/routes/_view/product/ai-notetaking.tsx +++ b/apps/web/src/routes/_view/product/ai-notetaking.tsx @@ -7,7 +7,7 @@ import { SearchIcon, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { memo, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { Typewriter } from "@hypr/ui/components/ui/typewriter"; import { cn } from "@hypr/utils"; @@ -2094,6 +2094,113 @@ const floatingPanelTabs = [ }, ]; +const PROGRESS_DURATION = 5000; + +function useAutoProgress({ + itemCount, + selectedIndex, + onSelect, + duration = PROGRESS_DURATION, +}: { + itemCount: number; + selectedIndex: number; + onSelect: (index: number) => void; + duration?: number; +}) { + const [progress, setProgress] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const startTimeRef = useRef(Date.now()); + const pausedProgressRef = useRef(0); + + const pause = useCallback(() => { + pausedProgressRef.current = progress; + setIsPaused(true); + }, [progress]); + + const resume = useCallback(() => { + startTimeRef.current = Date.now() - pausedProgressRef.current * duration; + setIsPaused(false); + }, [duration]); + + const goToIndex = useCallback( + (index: number) => { + onSelect(index); + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + }, + [onSelect], + ); + + useEffect(() => { + if (isPaused) return; + + const animate = () => { + const elapsed = Date.now() - startTimeRef.current; + const newProgress = Math.min(elapsed / duration, 1); + setProgress(newProgress); + + if (newProgress >= 1) { + const nextIndex = (selectedIndex + 1) % itemCount; + onSelect(nextIndex); + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + } + }; + + const intervalId = setInterval(animate, 50); + return () => clearInterval(intervalId); + }, [isPaused, selectedIndex, itemCount, onSelect, duration]); + + useEffect(() => { + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + }, [selectedIndex]); + + return { progress, isPaused, pause, resume, goToIndex }; +} + +function ProgressBar({ + itemCount, + selectedIndex, + progress, + onSelect, + className, +}: { + itemCount: number; + selectedIndex: number; + progress: number; + onSelect: (index: number) => void; + className?: string; +}) { + return ( +
+ {Array.from({ length: itemCount }).map((_, index) => ( + + ))} +
+ ); +} + function FloatingPanelSection() { return (
@@ -2128,14 +2235,27 @@ function FloatingPanelContent() { const [selectedTab, setSelectedTab] = useState(0); const scrollRef = useRef(null); - const scrollToTab = (index: number) => { + const scrollToTab = useCallback((index: number) => { setSelectedTab(index); if (scrollRef.current) { const container = scrollRef.current; const scrollLeft = container.offsetWidth * index; container.scrollTo({ left: scrollLeft, behavior: "smooth" }); } - }; + }, []); + + const { progress, pause, resume, goToIndex } = useAutoProgress({ + itemCount: floatingPanelTabs.length, + selectedIndex: selectedTab, + onSelect: scrollToTab, + }); + + const handleSelect = useCallback( + (index: number) => { + goToIndex(index); + }, + [goToIndex], + ); return (
@@ -2143,33 +2263,76 @@ function FloatingPanelContent() { scrollRef={scrollRef} selectedTab={selectedTab} setSelectedTab={setSelectedTab} - scrollToTab={scrollToTab} + progress={progress} + onSelect={handleSelect} + onPause={pause} + onResume={resume} /> + -
); } function FloatingPanelTablet({ selectedTab, - setSelectedTab, + progress, + onSelect, + onPause, + onResume, }: { selectedTab: number; - setSelectedTab: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { + const tabsContainerRef = useRef(null); + + useEffect(() => { + if (tabsContainerRef.current) { + const container = tabsContainerRef.current; + const selectedButton = container.children[0]?.children[ + selectedTab + ] as HTMLElement; + if (selectedButton) { + selectedButton.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + } + }, [selectedTab]); + return ( -
+
-
+
{floatingPanelTabs.map((tab, index) => (
+ +
); } -function FloatingPanelDesktop() { - const [selectedTab, setSelectedTab] = useState(0); +function FloatingPanelDesktop({ + selectedTab, + progress, + onSelect, + onPause, + onResume, +}: { + selectedTab: number; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; +}) { + const tabsContainerRef = useRef(null); + + useEffect(() => { + if (tabsContainerRef.current) { + const selectedElement = tabsContainerRef.current.children[ + selectedTab + ] as HTMLElement; + if (selectedElement) { + selectedElement.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + } + } + }, [selectedTab]); return ( -
+
-
+
{floatingPanelTabs.map((tab, index) => (
setSelectedTab(index)} + onClick={() => onSelect(index)} className={cn([ "p-6 cursor-pointer transition-colors", - index < tabs.length - 1 && "border-b border-neutral-100", + index < floatingPanelTabs.length - 1 && + "border-b border-neutral-100", selectedTab === index ? "bg-stone-50" : "hover:bg-neutral-50", ])} > @@ -2227,11 +2432,21 @@ function FloatingPanelDesktop() {
-
- {`${floatingPanelTabs[selectedTab].title} +
+ {`${floatingPanelTabs[selectedTab].title} +
+ +
@@ -2242,15 +2457,25 @@ function FloatingPanelMobile({ scrollRef, selectedTab, setSelectedTab, - scrollToTab, + progress, + onSelect, + onPause, + onResume, }: { scrollRef: React.RefObject; selectedTab: number; setSelectedTab: (index: number) => void; - scrollToTab: (index: number) => void; + progress: number; + onSelect: (index: number) => void; + onPause: () => void; + onResume: () => void; }) { return ( -
+
-
- {tabs.map((_, index) => ( -
+
); } From ee72fd0a2124ac8ecca3f8078db883528beaac70 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 02:32:20 +0000 Subject: [PATCH 2/2] fix(web): fix TypeScript errors in ai-notetaking.tsx and vs/$slug.tsx Co-Authored-By: john@hyprnote.com --- .../routes/_view/product/ai-notetaking.tsx | 33 ------ apps/web/src/routes/_view/vs/$slug.tsx | 102 +++++++++++++++++- 2 files changed, 99 insertions(+), 36 deletions(-) diff --git a/apps/web/src/routes/_view/product/ai-notetaking.tsx b/apps/web/src/routes/_view/product/ai-notetaking.tsx index 9b20cb9377..19b6552eaf 100644 --- a/apps/web/src/routes/_view/product/ai-notetaking.tsx +++ b/apps/web/src/routes/_view/product/ai-notetaking.tsx @@ -41,39 +41,6 @@ export const Route = createFileRoute("/_view/product/ai-notetaking")({ }), }); -const tabs = [ - { - title: "Compact Mode", - description: - "The default collapsed overlay that indicates the meeting is being listened to. Minimal and unobtrusive, staying out of your way.", - image: "/api/images/hyprnote/float-compact.jpg", - }, - { - title: "Memos", - description: - "Take quick notes during the meeting. Jot down important points, ideas, or reminders without losing focus on the conversation.", - image: "/api/images/hyprnote/float-memos.jpg", - }, - { - title: "Transcript", - description: - "Watch the live transcript as the conversation unfolds in real-time, so you never miss what was said during the meeting.", - image: "/api/images/hyprnote/float-transcript.jpg", - }, - { - title: "Live Insights", - description: - "Get a rolling summary of the past 5 minutes with AI-powered suggestions. For sales calls, receive prompts for qualification questions and next steps.", - image: "/api/images/hyprnote/float-insights.jpg", - }, - { - title: "Chat", - description: - "Ask questions and get instant answers during the meeting. Query the transcript, get clarifications, or find specific information on the fly.", - image: "/api/images/hyprnote/float-chat.jpg", - }, -]; - function Component() { return (
void; + duration?: number; +}) { + const [progress, setProgress] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const startTimeRef = useRef(Date.now()); + const pausedProgressRef = useRef(0); + + const pause = useCallback(() => { + pausedProgressRef.current = progress; + setIsPaused(true); + }, [progress]); + + const resume = useCallback(() => { + startTimeRef.current = Date.now() - pausedProgressRef.current * duration; + setIsPaused(false); + }, [duration]); + + const goToIndex = useCallback( + (index: number) => { + onSelect(index); + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + }, + [onSelect], + ); + + useEffect(() => { + if (isPaused) return; + + const animate = () => { + const elapsed = Date.now() - startTimeRef.current; + const newProgress = Math.min(elapsed / duration, 1); + setProgress(newProgress); + + if (newProgress >= 1) { + const nextIndex = (selectedIndex + 1) % itemCount; + onSelect(nextIndex); + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + } + }; + + const intervalId = setInterval(animate, 50); + return () => clearInterval(intervalId); + }, [isPaused, selectedIndex, itemCount, onSelect, duration]); + + useEffect(() => { + setProgress(0); + startTimeRef.current = Date.now(); + pausedProgressRef.current = 0; + }, [selectedIndex]); + + return { progress, isPaused, pause, resume, goToIndex }; +} + export const Route = createFileRoute("/_view/vs/$slug")({ component: Component, loader: async ({ params }) => { @@ -70,6 +138,28 @@ function Component() { } }; + const featuresProgress = useAutoProgress({ + itemCount: 5, + selectedIndex: selectedFeature, + onSelect: scrollToFeature, + }); + + const detailsProgress = useAutoProgress({ + itemCount: 5, + selectedIndex: selectedDetail, + onSelect: scrollToDetail, + }); + + const handleFeatureSelect = (index: number) => { + featuresProgress.goToIndex(index); + scrollToFeature(index); + }; + + const handleDetailSelect = (index: number) => { + detailsProgress.goToIndex(index); + scrollToDetail(index); + }; + return (