diff --git a/apps/web/public/avatar.webp b/apps/web/public/avatar.webp new file mode 100644 index 0000000000..10cfb8cc6c Binary files /dev/null and b/apps/web/public/avatar.webp differ diff --git a/apps/web/public/contact_human.webp b/apps/web/public/contact_human.webp new file mode 100644 index 0000000000..19fa17e834 Binary files /dev/null and b/apps/web/public/contact_human.webp differ diff --git a/apps/web/public/handdrawing/bracket-left.svg b/apps/web/public/handdrawing/bracket-left.svg new file mode 100644 index 0000000000..9344c703d8 --- /dev/null +++ b/apps/web/public/handdrawing/bracket-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/handdrawing/bracket-right.svg b/apps/web/public/handdrawing/bracket-right.svg new file mode 100644 index 0000000000..caa86b3bc1 --- /dev/null +++ b/apps/web/public/handdrawing/bracket-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/handdrawing/char-signature.svg b/apps/web/public/handdrawing/char-signature.svg new file mode 100644 index 0000000000..287add07c2 --- /dev/null +++ b/apps/web/public/handdrawing/char-signature.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/handdrawing/hyprnote-signature.svg b/apps/web/public/handdrawing/hyprnote-signature.svg new file mode 100644 index 0000000000..cef6ee4727 --- /dev/null +++ b/apps/web/public/handdrawing/hyprnote-signature.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/handdrawing/important.svg b/apps/web/public/handdrawing/important.svg new file mode 100644 index 0000000000..05af89d41d --- /dev/null +++ b/apps/web/public/handdrawing/important.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/handdrawing/scribbling.svg b/apps/web/public/handdrawing/scribbling.svg new file mode 100644 index 0000000000..1bb15962d1 --- /dev/null +++ b/apps/web/public/handdrawing/scribbling.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/handdrawing/sleeping.svg b/apps/web/public/handdrawing/sleeping.svg new file mode 100644 index 0000000000..1e5ae0efc3 --- /dev/null +++ b/apps/web/public/handdrawing/sleeping.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/handdrawing/sunset.svg b/apps/web/public/handdrawing/sunset.svg new file mode 100644 index 0000000000..9afcb56cd7 --- /dev/null +++ b/apps/web/public/handdrawing/sunset.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/web/public/handdrawing/thinking.svg b/apps/web/public/handdrawing/thinking.svg new file mode 100644 index 0000000000..773bc73906 --- /dev/null +++ b/apps/web/public/handdrawing/thinking.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/icons/grabbed-cursor.svg b/apps/web/public/icons/grabbed-cursor.svg new file mode 100644 index 0000000000..55b3f7f9d2 --- /dev/null +++ b/apps/web/public/icons/grabbed-cursor.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/mock-chat-input.tsx b/apps/web/src/components/mock-chat-input.tsx new file mode 100644 index 0000000000..38b1564792 --- /dev/null +++ b/apps/web/src/components/mock-chat-input.tsx @@ -0,0 +1,88 @@ +import { Icon } from "@iconify-icon/react"; +import { useEffect, useRef, useState } from "react"; + +import { cn } from "@hypr/utils"; + +const DEFAULT_PROMPTS = [ + "What are my action items from that meeting?", + "Summarize the key decisions we made today", + "What did Sarah say about the project timeline?", + "List all tasks assigned to me this week", + "What were the main blockers discussed?", +]; + +export function MockChatInput({ + prompts = DEFAULT_PROMPTS, + typingSpeed = 40, + pauseBetween = 2000, + className, +}: { + prompts?: string[]; + typingSpeed?: number; + pauseBetween?: number; + className?: string; +}) { + const [displayText, setDisplayText] = useState(""); + const [promptIndex, setPromptIndex] = useState(0); + const [isTyping, setIsTyping] = useState(true); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + let charIndex = 0; + const currentPrompt = prompts[promptIndex]; + + const typeNext = () => { + if (charIndex < currentPrompt.length) { + charIndex++; + setDisplayText(currentPrompt.slice(0, charIndex)); + timeoutRef.current = setTimeout(typeNext, typingSpeed); + } else { + setIsTyping(false); + timeoutRef.current = setTimeout(() => { + setDisplayText(""); + setIsTyping(true); + setPromptIndex((prev) => (prev + 1) % prompts.length); + }, pauseBetween); + } + }; + + setIsTyping(true); + timeoutRef.current = setTimeout(typeNext, 400); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [promptIndex, prompts, typingSpeed, pauseBetween]); + + return ( +
+
+ {displayText} + {isTyping && ( + + )} +
+ +
+
+ +
+
+
+ ); +} diff --git a/apps/web/src/components/mock-window.tsx b/apps/web/src/components/mock-window.tsx index e12507982e..f3735cac5c 100644 --- a/apps/web/src/components/mock-window.tsx +++ b/apps/web/src/components/mock-window.tsx @@ -7,6 +7,8 @@ export function MockWindow({ className, title, prefixIcons, + headerClassName, + audioIndicatorColor, children, }: { showAudioIndicator?: boolean; @@ -14,6 +16,8 @@ export function MockWindow({ className?: string; title?: string; prefixIcons?: React.ReactNode; + headerClassName?: string; + audioIndicatorColor?: string; children: React.ReactNode; }) { const isMobile = variant === "mobile"; @@ -26,7 +30,12 @@ export function MockWindow({ className, ])} > -
+
@@ -50,7 +59,7 @@ export function MockWindow({
)} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 413b094d8a..9aa3380291 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -2583,8 +2583,6 @@ declare module '@tanstack/react-router' { fullPath: '/api/admin/kanban/create' preLoaderRoute: typeof ApiAdminKanbanCreateRouteImport parentRoute: typeof rootRouteImport - } - parentRoute: typeof rootRouteImport } '/api/admin/import/google-docs': { id: '/api/admin/import/google-docs' diff --git a/apps/web/src/routes/_view/index.tsx b/apps/web/src/routes/_view/index.tsx index 4a4cf3f937..33270057de 100644 --- a/apps/web/src/routes/_view/index.tsx +++ b/apps/web/src/routes/_view/index.tsx @@ -4,8 +4,24 @@ import { useForm } from "@tanstack/react-form"; import { useMutation } from "@tanstack/react-query"; import { createFileRoute, Link } from "@tanstack/react-router"; import { allArticles } from "content-collections"; -import { useCallback, useEffect, useRef, useState } from "react"; - +import { CheckIcon } from "lucide-react"; +import { + AnimatePresence, + motion, + useMotionValue, + useMotionValueEvent, + useScroll, + useTransform, +} from "motion/react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { DancingSticks } from "@hypr/ui/components/ui/dancing-sticks"; import { cn } from "@hypr/utils"; import { DownloadButton } from "@/components/download-button"; @@ -14,6 +30,7 @@ import { GithubStars } from "@/components/github-stars"; import { Image } from "@/components/image"; import { LogoCloud } from "@/components/logo-cloud"; import { FAQ, FAQItem } from "@/components/mdx-jobs"; +import { MockChatInput } from "@/components/mock-chat-input"; import { MockWindow } from "@/components/mock-window"; import { SlashSeparator } from "@/components/slash-separator"; import { SocialCard } from "@/components/social-card"; @@ -120,23 +137,15 @@ function Component() { >
- + - + - + - + @@ -144,8 +153,6 @@ function Component() { - - @@ -396,6 +403,330 @@ function HeroSection({ ); } +type ScrollEffect = "opacity" | "blur" | "blurUp"; + +interface ScrollRevealWordProps { + progress: ReturnType>; + range: [number, number]; + effect: ScrollEffect; + children: React.ReactNode; +} + +function ScrollRevealWord({ + progress, + range, + effect, + children, +}: ScrollRevealWordProps) { + const [rangeStart, rangeEnd] = range; + const adjustedStart = Math.max(0, rangeStart - 0.05); + + const opacity = useTransform(progress, [adjustedStart, rangeEnd], [0.15, 1]); + const filter = useTransform( + progress, + [adjustedStart, rangeEnd], + ["blur(4px)", "blur(0px)"], + ); + const y = useTransform(progress, [adjustedStart, rangeEnd], [5, 0]); + + const style = useMemo(() => { + if (effect === "opacity") { + return { opacity }; + } + if (effect === "blur") { + return { opacity, filter }; + } + if (effect === "blurUp") { + return { opacity, filter, y, display: "inline-block" as const }; + } + return {}; + }, [effect, opacity, filter, y]); + + if (effect === "opacity" || effect === "blur" || effect === "blurUp") { + return {children}; + } + + return {children}; +} + +interface ScrollRevealParagraphProps { + children: React.ReactNode; + effect?: ScrollEffect; + className?: string; +} + +function ScrollRevealParagraph({ + children, + effect = "blur", + className, +}: ScrollRevealParagraphProps) { + const containerRef = useRef(null); + const { scrollYProgress } = useScroll({ + target: containerRef, + offset: ["start 0.5", "end 0.3"], + }); + + const ratchetedProgress = useMotionValue(0); + + useMotionValueEvent(scrollYProgress, "change", (latest) => { + if (latest > ratchetedProgress.get()) { + ratchetedProgress.set(latest); + } + }); + + const extractText = (node: React.ReactNode): string => { + if (typeof node === "string") return node; + if (typeof node === "number") return String(node); + if (Array.isArray(node)) return node.map(extractText).join(" "); + if (React.isValidElement(node)) { + const element = node as React.ReactElement; + if (element.props.children) { + return extractText(element.props.children); + } + } + return ""; + }; + + const allText = extractText(children); + const allWords = allText.split(/\s+/).filter((w) => w.length > 0); + const wordCount = allWords.length; + + let globalWordIndex = 0; + + const processNode = (node: React.ReactNode): React.ReactNode => { + if (typeof node === "string") { + const words = node.split(/(\s+)/); + return words.map((segment, i) => { + if (segment.trim().length === 0) { + return segment; + } + + const currentWordIndex = globalWordIndex; + globalWordIndex++; + + const start = currentWordIndex / wordCount; + const end = (currentWordIndex + 1) / wordCount; + + return ( + + {segment} + + ); + }); + } + + if (React.isValidElement(node)) { + const element = node as React.ReactElement; + + if (element.type === "img") { + const neighborIndex = Math.max(0, globalWordIndex - 1); + const start = neighborIndex / wordCount; + const end = (neighborIndex + 1) / wordCount; + + return ( + + {element} + + ); + } + + if (element.props.style?.backgroundImage) { + const innerText = extractText(element); + const innerWords = innerText.split(/\s+/).filter((w) => w.length > 0); + const startIndex = globalWordIndex; + globalWordIndex += innerWords.length; + const start = startIndex / wordCount; + const end = (startIndex + innerWords.length) / wordCount; + + return ( + + {element} + + ); + } + + return React.cloneElement(element, { + ...element.props, + children: React.Children.map(element.props.children, processNode), + }); + } + + if (Array.isArray(node)) { + return node.map((child, i) => ( + {processNode(child)} + )); + } + + return node; + }; + + return ( +
+ {processNode(children)} +
+ ); +} + +function HeroParagraphSection({ + onVideoExpand, +}: { + onVideoExpand: (id: string) => void; +}) { + return ( +
+
+

+ We believe in the power of notetaking, not notetakers. Meetings should + be moments of presence, not passive attendance.{" "} + Presence +

+ +

+ AI changes it. Instead of{" "} + + {" "} + scribbling{" "} + {" "} + notes, it gives us the power to be present. +

+

+ But we give it control over our meetings. What happens with all our + calls and chats then? Services sunset{" "} + + Sunset + {" "} + constantly, models change, progress is unstoppable. +

+

+ We believe in owning your data, doesn't matter where it lives. More + + {" "} + important{" "} + {" "} + is what you bring from every meeting, every call, every chat. +

+

+ + bracket left + Char + bracket right + {" "} + exists to preserve what makes us human: conversations that spark + ideas, collaborations that move work forward. We build tools that + amplify human agency, not replace it. +

+

+ No ghost bots. No silent note lurkers. Just people,{" "} + + thinking{" "} + thinking{" "} + + together. +

+
+ +
+
+
+ John Jeong + Yujong Lee +
+ +
+

+ Hyprnote +

+

John Jeong, Yujong Lee

+
+ +
+ Hyprnote Signature +
+
+ +
+ onVideoExpand(MUX_PLAYBACK_ID)} + /> +
+
+
+
+ ); +} + function ValuePropsGrid({ valueProps, }: { @@ -431,7 +762,7 @@ function TestimonialsSection() { return (
-

+

Loved by professionals at

@@ -712,6 +1043,9 @@ export function HowItWorksSection() { const [typedText1, setTypedText1] = useState(""); const [typedText2, setTypedText2] = useState(""); const [enhancedLines, setEnhancedLines] = useState(0); + const [activeTab, setActiveTab] = useState< + "notes" | "summary" | "transcription" + >("notes"); const text1 = "metrisc w/ john"; const text2 = "stakehlder mtg"; @@ -721,6 +1055,7 @@ export function HowItWorksSection() { setTypedText1(""); setTypedText2(""); setEnhancedLines(0); + setActiveTab("notes"); let currentIndex1 = 0; setTimeout(() => { @@ -740,27 +1075,31 @@ export function HowItWorksSection() { clearInterval(interval2); setTimeout(() => { - setEnhancedLines(1); + setActiveTab("summary"); + setTimeout(() => { - setEnhancedLines(2); + setEnhancedLines(1); setTimeout(() => { - setEnhancedLines(3); + setEnhancedLines(2); setTimeout(() => { - setEnhancedLines(4); + setEnhancedLines(3); setTimeout(() => { - setEnhancedLines(5); + setEnhancedLines(4); setTimeout(() => { - setEnhancedLines(6); + setEnhancedLines(5); setTimeout(() => { - setEnhancedLines(7); - setTimeout(() => runAnimation(), 1000); + setEnhancedLines(6); + setTimeout(() => { + setEnhancedLines(7); + setTimeout(() => runAnimation(), 2000); + }, 800); }, 800); }, 800); }, 800); }, 800); }, 800); - }, 800); - }, 500); + }, 300); + }, 800); } }, 50); } @@ -773,23 +1112,48 @@ export function HowItWorksSection() { return (
-
-

- How it works +

+

+ All your meetings stay yours. +

+

+ We believe that file is more important than software.
+ All saves locally, in plain markdown + .md

+
-
-
-
-

- While you take notes, Char - listens and keeps track of everything that happens during the - meeting. -

+
+ +
+ {(["notes", "summary", "transcription"] as const).map((tab) => ( + + ))}
-
- -
+ +
+ {activeTab === "notes" && ( +
ui update - moble
api
new dash - urgnet
@@ -807,21 +1171,10 @@ export function HowItWorksSection() { )}
- -
-
+ )} -
-
-

- After the meeting is over,{" "} - Char combines your notes with transcripts to create a perfect - summary. -

-
-
- -
+ {activeTab === "summary" && ( +

  • = 2 ? "opacity-100" : "opacity-0", - )} + ])} > Sarah presented the new mobile UI update, which includes a streamlined navigation bar and improved button placements @@ -895,149 +1248,400 @@ export function HowItWorksSection() {

- + )} + + {activeTab === "transcription" && ( +
+
+ Sarah:{" "} + So the mobile UI update is looking good. We've streamlined the + nav bar and improved button placements. +
+
+ Ben:{" "} + I'll need to adjust the API to support dynamic UI changes, + especially for personalized user data. +
+
+ Alice:{" "} + The new dashboard is urgent. Stakeholders have been asking + about it every day. +
+
+ Mark: We + should align the dashboard launch with our marketing push next + quarter. +
+
+ )}
-
+
-
-
-
-

- While you take notes, Char - listens and keeps track of everything that happens during the - meeting. +

+
+
+ +
+ +
+
+
+

+ Use local models or use Your Own key +

+

+ Hyprnote work with various transcription models right on your + device, even without internet.

-
- -
-
ui update - moble
-
api
-
new dash - urgnet
-
a/b tst next wk
-
- {typedText1} - {typedText1 && typedText1.length < text1.length && ( - | - )} +
+ +
+
+
+
+ +
+ +
+ + +
+

+ Meeting.12.03.26-11.32.wav +

+

14:30:25

-
- {typedText2} - {typedText2 && typedText2.length < text2.length && ( - | - )} +
+
+
+
+

+ Upload records or existing transcripts +

+

+ Hyprnote work with various transcription models right on your + device, even without internet +

+
+
+ +
+
+
+
+ +
+

1-1 with Joanna

+

+ AI Notetaker joined the call. +

- + +
+
+ +
+

+ No bot on calls +

+

+ Char is connecting right to your system audio and get every word + perfectly, no faceless bots join your meetings. +

+
+
+ ); +} -
-
-

- After the meeting is over,{" "} - Char combines your notes with transcripts to create a perfect - summary. +export function AISection() { + const researchStatuses = [ + "Exploring meetings", + "Analysing", + "Generating summary", + ]; + const [statusIndex, setStatusIndex] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setStatusIndex((prev) => (prev + 1) % researchStatuses.length); + }, 1200); + return () => clearInterval(interval); + }, []); + + return ( +

+
+

+ Get more from every note with AI assistant +

+

+ Ask, give tasks to execute and grow your team knowledge base. +

+ + More about AI + +
+
+ +
+ +
+
+
+ {[ + { name: "Slack", image: "slack.jpg" }, + { name: "Linear", image: "linear.jpg" }, + { name: "Notion", image: "notion.jpg" }, + ].map((integration) => ( +
+ {integration.name} +
+ ))} +
+
+

+ Workflows and integrations +

+

+ Automate follow-up tasks across your tools without manual data + entry.

-
- -
-
-

- Mobile UI Update and API Adjustments -

-
    -
  • = 1 ? "opacity-100" : "opacity-0", - ])} - > - Sarah presented the new mobile UI update, which includes a - streamlined navigation bar and improved button placements - for better accessibility. -
  • -
  • = 2 ? "opacity-100" : "opacity-0", - ])} - > - Ben confirmed that API adjustments are needed to support - dynamic UI changes, particularly for fetching personalized - user data more efficiently. -
  • -
  • = 3 ? "opacity-100" : "opacity-0", - ])} +
+ +
+
+
+
+ + What was John's tasks from previous call? + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+

+ Ask questions in realtime +

+

+ Get instant answers from your current calls and previous meetings. +

+
+
+ +
+
+
+
+ JJ +
+
+

John Jeong's progress

+
+ + - The UI update will be implemented in phases, starting with - core navigation improvements. Ben will ensure API - modifications are completed before development begins. - - + {researchStatuses[statusIndex]} + +
-
-

- New Dashboard – Urgent Priority +

+
+
+
+

+ Deep research of your chats +

+

+ Chat with your AI assistant to learn more about the people you're + meeting with. +

+
+
+
+
+ ); +} + +export function GrowsWithYouSection() { + return ( +
+
+

+ Char grows with you +

+

+ Add people from meetings in contacts, grow knowledge about your chats + and context of previous meetings +

+ + Explore all features + + +
+ +
+
+
+

+ Your contacts in one place +

+

+ Import contacts and watch them come alive with context once you + actually meet. +

+
    +
  • + + + All your chats linked + +
  • +
  • + + + Generated summary from meetings + +
  • +
+
+
+ Contacts interface +
+
+ +
+
+

+ Calendar +

+

+ Connect your calendar for intelligent meeting preparation and + automatic note organization. +

+
    +
  • + + + Automatic meeting linking + +
  • +
  • + + + Pre-meeting context and preparation + +
  • +
  • + + + Timeline view with notes + +
  • +
+
+ +
+
+
+ +
+

+ Weekly Team Sync

-
    -
  • = 4 ? "opacity-100" : "opacity-0", - ])} - > - Alice emphasized that the new analytics dashboard must be - prioritized due to increasing stakeholder demand. -
  • -
  • = 5 ? "opacity-100" : "opacity-0", - ])} - > - The new dashboard will feature real-time user engagement - metrics and a customizable reporting system. -
  • -
  • = 6 ? "opacity-100" : "opacity-0", - ])} - > - Ben mentioned that backend infrastructure needs - optimization to handle real-time data processing. -
  • -
  • = 6 ? "opacity-100" : "opacity-0", - ])} - > - Mark stressed that the dashboard launch should align with - marketing efforts to maximize user adoption. -
  • -
  • = 6 ? "opacity-100" : "opacity-0", - ])} - > - Development will start immediately, and a basic prototype - must be ready for stakeholder review next week. -
  • -
+

+ Today at 10:00 AM · 30 minutes +

+
- +
+
+
+ Last meeting context +
+
+
+ Jan 8, 2025 - Weekly Team Sync +
+

+ Discussed Q1 roadmap, decided to prioritize mobile app. + Sarah to review designs by Jan 15. +

+
+
+
+
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 4e5aa0c183..8e9b4933b7 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -262,6 +262,30 @@ .animate-fade-in-out { animation: fade-in-out 3s ease-in-out infinite; } + + @keyframes shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } + } + + .text-shimmer { + background: linear-gradient( + 90deg, + #a3a3a3 0%, + #525252 50%, + #a3a3a3 100% + ); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer 2s ease-in-out infinite; + } } @layer base { diff --git a/package.json b/package.json index b1761ac104..81726d5891 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "scripts": { + "dev": "pnpm --filter @hypr/web run dev", "fmt:check": "echo 'TODO'", "fmt": "dprint fmt", "lint": "oxlint --type-aware || eslint"