diff --git a/apps/web/content/articles/mac-productivity-apps.mdx b/apps/web/content/articles/mac-productivity-apps.mdx index e2db53b9d7..0889081e9f 100644 --- a/apps/web/content/articles/mac-productivity-apps.mdx +++ b/apps/web/content/articles/mac-productivity-apps.mdx @@ -13,7 +13,7 @@ author: - "n" - "g" featured: false -category: "Productivity Hack" +category: "Guides" date: "2026-02-13" --- 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/hero-interface.tsx b/apps/web/src/components/hero-interface.tsx new file mode 100644 index 0000000000..892887c26b --- /dev/null +++ b/apps/web/src/components/hero-interface.tsx @@ -0,0 +1,221 @@ +import { Icon } from "@iconify-icon/react"; +import { useState } from "react"; + +import { DancingSticks } from "@hypr/ui/components/ui/dancing-sticks"; +import { cn } from "@hypr/utils"; + +export function HeroInterface() { + const [activeTab, setActiveTab] = useState< + "notes" | "summary" | "transcript" + >("notes"); + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Today +
+
+ + + + +
+
+ Yesterday +
+
+ + + +
+ +
+
+
+ + alex@company.com + +
+ +
+
+ {/* tabs */} +
+
+
+ + Weekly Product Sync + +
+
+ + Design Review +
+
+ +
+
+
+ Weekly Product Sync +
+
+ +
+ {(["notes", "summary", "transcript"] as const).map((tab) => ( + + ))} +
+ +
+ {activeTab === "notes" && ( +
+
ui update - mobile
+
api changes for new nav
+
new dash - urgnet
+
a/b tst next wk
+
sarah handles design tokens
+
+ ben on api by friday + | +
+
+ )} + {activeTab === "summary" && ( +
+
+

+ Mobile UI Update +

+
    +
  • + Streamlined navigation bar with improved button + placements for accessibility. +
  • +
  • + API adjustments needed for dynamic UI and personalized + user data. +
  • +
+
+
+

+ New Dashboard +

+
    +
  • + Urgent priority due to stakeholder demand for + analytics. +
  • +
  • + Real-time engagement metrics and customizable + reporting. +
  • +
+
+
+ )} + {activeTab === "transcript" && ( +
+
+ + Sarah: + {" "} + 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 changes, + especially for personalized data. +
+
+ + Alice: + {" "} + The new dashboard is urgent. Stakeholders keep asking + about it. +
+
+ + Mark: + {" "} + Let's align the dashboard launch with our marketing push + next quarter. +
+
+ )} +
+
+
+
+
+
+ ); +} 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..a6a2857ce4 --- /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..bd13546788 100644 --- a/apps/web/src/routes/_view/index.tsx +++ b/apps/web/src/routes/_view/index.tsx @@ -4,16 +4,34 @@ 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"; import { GitHubOpenSource } from "@/components/github-open-source"; import { GithubStars } from "@/components/github-stars"; +import { HeroInterface } from "@/components/hero-interface"; 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 +138,15 @@ function Component() { >
- + - + - + - + @@ -144,8 +154,6 @@ function Component() { - - @@ -182,10 +190,8 @@ function YCombinatorBanner() { } function HeroSection({ - onVideoExpand, heroInputRef, }: { - onVideoExpand: (id: string) => void; heroInputRef: React.RefObject; }) { const platform = usePlatform(); @@ -253,7 +259,7 @@ function HeroSection({ }, [heroContext, handleTrigger]); return ( -
+
-
- onVideoExpand(MUX_PLAYBACK_ID)} - /> +
+ {/* */} +
+
+
+ ); +} -
- -
+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)} />
-
+
); } @@ -431,7 +749,7 @@ function TestimonialsSection() { return (
-

+

Loved by professionals at

@@ -712,6 +1030,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 +1042,7 @@ export function HowItWorksSection() { setTypedText1(""); setTypedText2(""); setEnhancedLines(0); + setActiveTab("notes"); let currentIndex1 = 0; setTimeout(() => { @@ -740,27 +1062,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 +1099,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 +1158,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 +1235,402 @@ 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/routes/_view/product/ai-assistant.tsx b/apps/web/src/routes/_view/product/ai-assistant.tsx index 80fc32931e..52ac67e904 100644 --- a/apps/web/src/routes/_view/product/ai-assistant.tsx +++ b/apps/web/src/routes/_view/product/ai-assistant.tsx @@ -1,9 +1,18 @@ import { Icon } from "@iconify-icon/react"; import { createFileRoute, Link } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { + AnimatePresence, + motion, + useMotionValue, + useTransform, +} from "motion/react"; +import { useEffect, useRef, useState } from "react"; +import { DancingSticks } from "@hypr/ui/components/ui/dancing-sticks"; import { cn } from "@hypr/utils"; +import { MockChatInput } from "@/components/mock-chat-input"; +import { MockWindow } from "@/components/mock-window"; import { SlashSeparator } from "@/components/slash-separator"; export const Route = createFileRoute("/_view/product/ai-assistant")({ @@ -21,6 +30,39 @@ export const Route = createFileRoute("/_view/product/ai-assistant")({ }), }); +const FEATURES = [ + { + title: "Ask about past conversations", + description: + "Query your entire conversation history to refresh your memory. Find decisions, action items, or specific topics discussed in previous meetings\u2014all in natural language.", + }, + { + title: "Execute workflows and tasks", + description: + "Describe what you want to do, and let your AI assistant handle the rest. Automate follow-up tasks across your tools without manual data entry.", + integrations: [ + { icon: "simple-icons:slack", label: "" }, + { icon: "simple-icons:linear", label: "" }, + { icon: "simple-icons:jira", label: "" }, + ], + }, + { + title: "Chat during meetings", + description: + "Get instant answers from the current transcript and past meeting context.", + }, + { + title: "Improve with every transcription", + description: + "Your AI assistant learns from every interaction, adapting to your preferences and continuously improving transcription accuracy and summary quality.", + }, + { + title: "Deep Research based on your meetings", + description: + "Search through past conversations, extract key insights, and understand context before you join.", + }, +]; + function Component() { return (
- + + + - + - +
@@ -44,199 +88,815 @@ function Component() { function HeroSection() { return ( -
-
+
+

AI Chat AI Chat for your meetings

-

+

Prepare, engage, and follow through with AI-powered assistance

-
- - Download for free - +
+
); } -function BeforeMeetingSection() { +const HEADER_HEIGHT = 69; + +function ScrollFeatureSection() { + const containerRef = useRef(null); + const pinnedRef = useRef(null); + const [activeIndex, setActiveIndex] = useState(0); + const scrollProgress = useMotionValue(0); + + useEffect(() => { + const onScroll = () => { + const container = containerRef.current; + const pinned = pinnedRef.current; + if (!container || !pinned) return; + + const rect = container.getBoundingClientRect(); + const viewH = window.innerHeight - HEADER_HEIGHT; + const containerH = container.offsetHeight; + + const scrolledPast = HEADER_HEIGHT - rect.top; + const maxScroll = containerH - viewH; + + if (scrolledPast <= 0) { + pinned.style.position = "absolute"; + pinned.style.top = "0px"; + pinned.style.bottom = "auto"; + pinned.style.left = "0"; + pinned.style.right = "0"; + pinned.style.width = ""; + pinned.style.height = `${viewH}px`; + } else if (scrolledPast >= maxScroll) { + pinned.style.position = "absolute"; + pinned.style.top = "auto"; + pinned.style.bottom = "0px"; + pinned.style.left = "0"; + pinned.style.right = "0"; + pinned.style.width = ""; + pinned.style.height = `${viewH}px`; + } else { + pinned.style.position = "fixed"; + pinned.style.top = `${HEADER_HEIGHT}px`; + pinned.style.bottom = "auto"; + pinned.style.left = `${rect.left}px`; + pinned.style.right = "auto"; + pinned.style.width = `${container.offsetWidth}px`; + pinned.style.height = `${viewH}px`; + } + + const progress = Math.max(0, Math.min(1, scrolledPast / maxScroll)); + const index = Math.min( + Math.floor(progress * FEATURES.length), + FEATURES.length - 1, + ); + setActiveIndex(index); + scrollProgress.set(progress); + }; + + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", onScroll, { passive: true }); + onScroll(); + return () => { + window.removeEventListener("scroll", onScroll); + window.removeEventListener("resize", onScroll); + }; + }); + + const scrollToFeature = (index: number) => { + const container = containerRef.current; + if (!container) return; + + const containerTop = container.getBoundingClientRect().top + window.scrollY; + const containerH = container.offsetHeight; + const viewH = window.innerHeight - HEADER_HEIGHT; + const maxScroll = containerH - viewH; + const segmentSize = maxScroll / FEATURES.length; + const targetScroll = + containerTop - + HEADER_HEIGHT + + (index / FEATURES.length) * maxScroll + + segmentSize * 0.1; + + window.scrollTo({ top: targetScroll, behavior: "smooth" }); + }; + return ( -
-
- Before meetings + <> + {/* Desktop */} +
+
+
+
+ {FEATURES.map((feature, index) => ( + scrollToFeature(index)} + > + +
+

+ {feature.title} +

+

+ {feature.description} +

+ {feature.integrations && ( +
+ {feature.integrations.map((item) => ( +
+ + {item.label} +
+ ))} +
+ )} +
+
+ ))} +
+
+ +
+ +
+
-
-
-
- -

- Deep research with chat + {/* Mobile */} +
+ {FEATURES.map((feature, index) => ( +
+

+ {feature.title}

-

- Chat with your AI assistant to learn more about the people you're - meeting with. Search through past conversations, extract key - insights, and understand context before you join. +

+ {feature.description}

-
    -
  • - - - "What did we discuss last time with Sarah?" - -
  • -
  • - - - "What are the client's main concerns?" - -
  • -
  • +
    + +
    +
+ ))} +
+ + ); +} + +function FeatureProgressBar({ + index, + activeIndex, + scrollProgress, + total, +}: { + index: number; + activeIndex: number; + scrollProgress: ReturnType>; + total: number; +}) { + const segmentStart = index / total; + const segmentEnd = (index + 1) / total; + + const scaleX = useTransform( + scrollProgress, + [segmentStart, segmentEnd], + [0, 1], + ); + + const isActive = index === activeIndex; + const isPast = index < activeIndex; + + return ( +
+ {isPast ? ( +
+ ) : isActive ? ( + + ) : null} +
+ ); +} + +type ChatStep = { + node: React.ReactNode | ((activeIndex: number) => React.ReactNode); + delay: number; +}; + +type ChatPanel = { + type: "chat"; + steps: ChatStep[]; + footer?: React.ReactNode; +}; + +type SpecialPanel = { + type: "special"; + content: React.ReactNode; +}; + +type Panel = ChatPanel | SpecialPanel; + +function SearchToolCall({ activeIndex }: { activeIndex: number }) { + const [phase, setPhase] = useState(0); + + useEffect(() => { + setPhase(0); + const t1 = setTimeout(() => setPhase(1), 800); + const t2 = setTimeout(() => setPhase(2), 1400); + return () => { + clearTimeout(t1); + clearTimeout(t2); + }; + }, [activeIndex]); + + const meetings = [ + "Weekly Sync — Oct 12", + "1:1 with Sarah — Oct 10", + "Sprint Planning — Oct 8", + ]; + + return ( +
+
+
+ + {phase < 2 ? "Searching meetings..." : "3 meetings found"} + +
+ + {phase >= 1 && ( + + {meetings.slice(0, phase >= 2 ? 3 : 1).map((m) => ( + - - "Show me all action items from previous meetings" - - - + {m} + + ))} + + )} + +
+ ); +} + +function JiraToolCall({ activeIndex }: { activeIndex: number }) { + const [phase, setPhase] = useState(0); + + useEffect(() => { + setPhase(0); + const t1 = setTimeout(() => setPhase(1), 600); + const t2 = setTimeout(() => setPhase(2), 1400); + const t3 = setTimeout(() => setPhase(3), 2000); + return () => { + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + }; + }, [activeIndex]); + + return ( +
+
+ + + {phase < 1 ? ( + + + Creating ticket... + + ) : ( + + ENG-247 + + Created + + + )} + +
+ + {phase >= 2 && ( + +

+ Mobile UI bug fix +

+
+ )} +
+ + {phase >= 3 && ( + +
+ S +
+ Sarah +
+ )} +
+
+ ); +} + +function TranscriptToolCall({ activeIndex }: { activeIndex: number }) { + const [phase, setPhase] = useState(0); + + useEffect(() => { + setPhase(0); + const t1 = setTimeout(() => setPhase(1), 500); + const t2 = setTimeout(() => setPhase(2), 1200); + return () => { + clearTimeout(t1); + clearTimeout(t2); + }; + }, [activeIndex]); + + return ( +
+
+ + {phase >= 1 && ( + + Sarah: + + The API changes will need at least two sprints... + + + )} + + + {phase >= 2 && ( + + Ben: + + I can start on the auth module this week. + + + )} + + {phase === 0 && ( +
+ + Reading transcript...
+ )} +
+
+ ); +} -
- -

- Generate custom templates -

-

- Create tailored meeting templates on the spot. Ask your AI - assistant to generate agendas, question lists, or note structures - specific to your meeting type. +const CHAT_PANELS: Panel[] = [ + { + type: "chat", + steps: [ + { + delay: 200, + node: ( +

+
+

+ What did Sarah say about the timeline? +

+
+
+ ), + }, + { + delay: 800, + node: (idx: number) => , + }, + { + delay: 3000, + node: ( +
+

Char

+

+ In your Oct 12 meeting, Sarah mentioned the deadline is Q1 2026 + with a soft launch in December.

-
    -
  • - - - "Create a customer discovery template" - -
  • -
  • - - - "Generate questions for a technical interview" - -
  • -
  • - - - "Build an agenda for our quarterly review" - -
  • -
+
+ ), + }, + ], + }, + { + type: "chat", + steps: [ + { + delay: 200, + node: ( +
+
+

+ Create a Jira ticket for the mobile bug and assign to Sarah +

+
+
+ ), + }, + { + delay: 800, + node: (idx: number) => , + }, + { + delay: 3200, + node: ( +
+

Char

+
+ + + Jira ticket ENG-247 created and assigned to Sarah. + +
+
+ ), + }, + ], + }, + { + type: "chat", + steps: [ + { + delay: 200, + node: ( +
+
+

+ What's the timeline for the mobile UI? +

+
+
+ ), + }, + { + delay: 800, + node: (idx: number) => , + }, + { + delay: 2800, + node: ( +
+

Char

+

+ Ben committed to auth module this week. Sarah estimates 2 sprints + for full API. +

+
+ ), + }, + ], + footer: ( +
+
+
+
+
+ Design weekly sync +
+
+ +
- -
+
+ ), + }, + { + type: "special", + content: ( +
+
+ + + Quality improving over time + +
+
+

Before

+

+ the team talked about doing stuff with the dashboard and some api + things +

+
+
+

After

+

+ The team agreed to prioritize the dashboard redesign and begin API + migration in Sprint 14. +

+
+
+ {[ + { label: "Accuracy", value: "94%" }, + { label: "Adapted", value: "12x" }, + ].map((stat) => ( +
+ {stat.value} + {stat.label} +
+ ))} +
+
+ ), + }, + { + type: "special", + content: ( +
+
+
+
+ JK +
+
+

Jennifer Kim

+

Product Manager

+
+
+
+ {["Q4 roadmap", "Mobile launch", "Budget review"].map((t) => ( + + {t} + + ))} +
+
+
+

+ Last 3 meetings focused on mobile launch timeline. Jennifer prefers + concise bullet-point summaries. +

+
+
-

- Ask about past conversations -

-

- Query your entire conversation history to refresh your memory. Find - decisions, action items, or specific topics discussed in previous - meetings—all in natural language. -

+ + 5 past meetings analyzed +
-

+ ), + }, +]; + +function ChatMessages({ + panel, + activeIndex, +}: { + panel: ChatPanel; + activeIndex: number; +}) { + const [visibleCount, setVisibleCount] = useState(0); + + useEffect(() => { + setVisibleCount(0); + const timers = panel.steps.map((step, i) => + setTimeout(() => setVisibleCount(i + 1), step.delay), + ); + return () => timers.forEach(clearTimeout); + }, [activeIndex, panel.steps]); + + return ( + + + {panel.steps.slice(0, visibleCount).map((step, i) => ( + + {typeof step.node === "function" + ? step.node(activeIndex) + : step.node} + + ))} + + ); } -function DuringMeetingSection() { +function FeatureVisual({ activeIndex }: { activeIndex: number }) { + const [inputValue, setInputValue] = useState(""); + const panel = CHAT_PANELS[activeIndex]; + const isChat = panel.type === "chat"; + const hasFooter = isChat && !!panel.footer; + return ( -
-
- During meetings -
+
+ + + {isChat ? ( + + ) : ( + + {panel.content} + + )} + -
-
-
- -

- Ask questions in realtime -

-

- Type questions to your AI assistant during the meeting without - interrupting the conversation. Get instant answers from the - current transcript and past meeting context. -

-
+ + {isChat && ( + + {hasFooter ? ( + panel.footer + ) : ( +
+
+ setInputValue(e.target.value)} + placeholder="Ask Char anything..." + className="flex-1 text-sm bg-transparent outline-none placeholder:text-neutral-400 text-stone-700" + /> +
+ +
+
+
+ )} +
+ )} +
+ +
+ ); +} -
- -

- Realtime insights via{" "} - - extensions - -

-

- AI-powered extensions provide live assistance during your meeting. - Built on our extension framework, these tools adapt to your needs - in realtime. -

+function ExtensionsSection() { + return ( +
+
+
+

+ Realtime insights via{" "} + + extensions + +

+

+ AI-powered extensions provide live assistance during your meeting. + Built on our extension framework, these tools adapt to your needs in + realtime. +

+
+ + Learn more about extensions + +
-
+
-

+

Available realtime extensions -

+

- -
- - Learn more about extensions - - -
@@ -295,243 +945,373 @@ function DuringMeetingSection() { ); } -function AfterMeetingSection() { - const slides = [ - { - prompt: - "Add a Jira ticket for the mobile UI bug and assign it to Sarah today", - card: ( -
-
- - ENG-247 - - Todo - -
-
- Mobile UI bug fix -
-

- Fix the mobile UI bug discussed in today's meeting. Check responsive - layout on iOS devices. -

-
-
- S -
- Sarah -
-
- ), - toolbar: "simple-icons:jira", - }, - { - prompt: "Send the summary to #engineering and update the Q4 roadmap now", - card: ( -
-
- - #engineering - · - 2:15 PM -
-
-

Jessica Lee

-

- Meeting summary attached as a file for review, including key - decisions, action items, and next steps for the Q4 rollout. -

-
- - meeting-summary.pdf -
-
-
- ), - toolbar: "simple-icons:slack", - }, - { - prompt: - "Schedule a follow-up next week with the client and share the agenda", - card: ( -
-
- - Mon, 9:30 AM - · - 30 min -
-
-

Follow-up meeting

-

- 2 guests · 1 yes, 1 awaiting -

-
-
- A -
- John Smith -
-
-
- M -
- Mudit Jain -
+const TEMPLATE_PROMPTS = [ + "Create a customer discovery template", + "Generate questions for a technical interview", + "Build an agenda for our quarterly review", +]; + +function TemplatesSection() { + return ( +
+
+

+ Generate custom templates +

+

+ Create tailored meeting templates on the spot. Ask your AI assistant + to generate agendas, question lists, or note structures specific to + your meeting type. +

+
+ +
+ {TEMPLATE_PROMPTS.map((prompt) => ( +
+ {prompt}
-
- ), - toolbar: "simple-icons:googlecalendar", - }, - ]; - const [activeIndex, setActiveIndex] = useState(0); - const [progress, setProgress] = useState(0); + ))} +
+
+ ); +} + +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"; useEffect(() => { - const interval = window.setInterval(() => { - setProgress((current) => { - const next = current + 2; - if (next >= 100) { - setActiveIndex( - (prevIndex) => (prevIndex - 1 + slides.length) % slides.length, - ); - return 0; - } - return next; - }); - }, 80); - - return () => window.clearInterval(interval); - }, [slides.length]); - - const activeSlide = slides[activeIndex]; + const runAnimation = () => { + setTypedText1(""); + setTypedText2(""); + setEnhancedLines(0); + setActiveTab("notes"); + + let currentIndex1 = 0; + setTimeout(() => { + const interval1 = setInterval(() => { + if (currentIndex1 < text1.length) { + setTypedText1(text1.slice(0, currentIndex1 + 1)); + currentIndex1++; + } else { + clearInterval(interval1); + + let currentIndex2 = 0; + const interval2 = setInterval(() => { + if (currentIndex2 < text2.length) { + setTypedText2(text2.slice(0, currentIndex2 + 1)); + currentIndex2++; + } else { + clearInterval(interval2); + + setTimeout(() => { + setActiveTab("summary"); + + setTimeout(() => { + setEnhancedLines(1); + setTimeout(() => { + setEnhancedLines(2); + setTimeout(() => { + setEnhancedLines(3); + setTimeout(() => { + setEnhancedLines(4); + setTimeout(() => { + setEnhancedLines(5); + setTimeout(() => { + setEnhancedLines(6); + setTimeout(() => { + setEnhancedLines(7); + setTimeout(() => runAnimation(), 2000); + }, 800); + }, 800); + }, 800); + }, 800); + }, 800); + }, 800); + }, 300); + }, 800); + } + }, 50); + } + }, 50); + }, 500); + }; + + runAnimation(); + }, []); return ( -
-
- After meetings +
+
+

+ How Char works +

+

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

+
+ +
+ {(["notes", "summary", "transcription"] as const).map((tab) => ( + + ))} +
-
-
-
- -

- Execute workflows with natural language -

-

- Describe what you want to do, and let your AI assistant handle the - rest. Automate follow-up tasks across your tools without manual - data entry. -

-
-
-
- "{activeSlide.prompt}" +
+ {activeTab === "notes" && ( +
+
ui update - moble
+
api
+
new dash - urgnet
+
a/b tst next wk
+
+ {typedText1} + {typedText1 && typedText1.length < text1.length && ( + | + )}
-
-
- {activeSlide.card} +
+ {typedText2} + {typedText2 && typedText2.length < text2.length && ( + | + )}
-
- {slides.map((slide, index) => ( - - ))} + Mobile UI Update and API Adjustments + +
    +
  • = 2 ? "opacity-100" : "opacity-0", + ])} + > + Sarah presented the new mobile UI update, which includes a + streamlined navigation bar and improved button placements + for better accessibility. +
  • +
  • = 3 ? "opacity-100" : "opacity-0", + ])} + > + Ben confirmed that API adjustments are needed to support + dynamic UI changes, particularly for fetching personalized + user data more efficiently. +
  • +
  • = 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. +
  • +
+
+
+

= 5 ? "opacity-100" : "opacity-0", + ])} + > + New Dashboard – Urgent Priority +

+
    +
  • = 6 ? "opacity-100" : "opacity-0", + ])} + > + Alice emphasized that the new analytics dashboard must be + prioritized due to increasing stakeholder demand. +
  • +
  • = 7 ? "opacity-100" : "opacity-0", + ])} + > + The new dashboard will feature real-time user engagement + metrics and a customizable reporting system. +
  • +
+
-
+ )} + + {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. +
+
+ )}
+ +
-
- -

- Learns and adapts with memory -

-

- Your AI assistant builds memory from your interactions. It - remembers preferences, learns from edits you make to summaries, - and continuously improves its assistance based on your patterns. +

+
+
+ +
+ +
+
+
+

+ Use local models or use Your Own key +

+

+ Char works with various transcription models right on your device, + even without internet.

-
    -
  • +
+
+ +
+
+
+
- - Remembers your meeting preferences and formats - - -
  • - + +
    + - - Learns from your edits to improve future summaries - -
  • -
  • - - Adapts to your workflow and tool preferences - -
  • -
  • +
    +

    + Meeting.12.03.26-11.32.wav +

    +

    14:30:25

    +
    +
  • +
    +
    +
    +

    + Upload records or existing transcripts +

    +

    + Drag and drop audio files or paste existing transcripts to + generate summaries instantly. +

    +
    +
    + +
    +
    +
    +
    - - Builds context about your team and projects over time - - - +
    +

    1-1 with Joanna

    +

    + AI Notetaker joined the call. +

    +
    +
    + +
    +
    + +
    +

    + No bot on calls +

    +

    + Char connects right to your system audio and captures every word + perfectly, no faceless bots join your meetings. +

    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"