diff --git a/.gitignore b/.gitignore index c84f02edfe..8e1e4f57fc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ supabase/.seed/ *storybook.log .cache/ +.cursor/ internal .turbo diff --git a/apps/web/content/solutions/founders.mdx b/apps/web/content/solutions/founders.mdx new file mode 100644 index 0000000000..b5b683c577 --- /dev/null +++ b/apps/web/content/solutions/founders.mdx @@ -0,0 +1,55 @@ +--- +label: "For Founders" +icon: "mdi:rocket-launch" +order: 2 +metaTitle: "AI Meeting Notes for Founders - Char" +metaDescription: "For founders running investor calls, customer discovery, team 1:1s, and board updates. Char keeps every conversation in one place with the context you need to act." +hero: + badgeText: "For Founders" + title: "One source of truth\nfor every conversation you run" + description: "Investor calls, customer discovery, team 1:1s, hiring loops, board updates. Char captures it all with zero overhead and keeps the context you actually need to act." + primaryCTA: + label: "Download for free" + to: "/download/" + secondaryCTA: + label: "See how it works" + to: "/product/ai-notetaking/" +features: + title: "Built for high-agency founders" + description: "Every founder has more context than time. Char keeps what happened, why it mattered, and what comes next." + items: + - icon: "mdi:source-branch" + title: "One source of truth" + description: "Collapse Notion docs, email threads, and Slack DMs into a single searchable memory tied to the meeting it came from." + - icon: "mdi:account-multiple-plus" + title: "Investor & customer memory" + description: "Never re-introduce yourself to an investor or repeat a discovery question. Char surfaces the last conversation before the next one starts." + - icon: "mdi:clipboard-list" + title: "Cross-context agendas" + description: "Switch from board prep to a customer call to a 1:1 without losing the thread. Char builds the agenda from what actually happened." + - icon: "mdi:robot-happy" + title: "Hand off to AI agents" + description: "Turn takeaways into follow-ups, drafts, and tickets — pass them to Claude, Cursor, or your own agents with one click." + - icon: "mdi:shield-lock" + title: "Your data, your rules" + description: "No bots on calls. Meetings stay on your device. Bring your own keys. Share only what you choose." + - icon: "mdi:chart-line" + title: "Built for speed, not seats" + description: "Designed for founders who move fast, without admin overhead, approval queues, or complicated permission models." +useCases: + title: "Every conversation a founder runs" + description: "From fundraising to hiring to shipping, Char keeps it all in one place." + items: + - title: "Investor calls" + description: "Capture every pitch, commitment, and follow-up. Walk into the next round with the full history in hand." + - title: "Customer discovery" + description: "Turn raw interview audio into themes, quotes, and a live picture of what customers actually want." + - title: "Team 1:1s & stand-ups" + description: "Remember what you promised, what they raised, and what changed between syncs." + - title: "Hiring & board updates" + description: "Interview loops, board prep, and investor updates drafted straight from the meetings that informed them." +cta: + title: "Run every conversation like your best one" + description: "Join founders using Char to keep the context that makes fast companies move." +--- + diff --git a/apps/web/package.json b/apps/web/package.json index 257198a6c2..330df4235e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,7 +7,8 @@ "build": "NODE_OPTIONS='--max-old-space-size=8192' vite build && cp public/sitemap.xml dist/client/sitemap.xml && pagefind --site ./dist/client", "serve": "vite preview", "test": "node --test", - "typecheck": "tsc --project tsconfig.json --noEmit", + "gen:content-collections": "node scripts/build-content-collections.js", + "typecheck": "pnpm run gen:content-collections && tsc --project tsconfig.json --noEmit", "gen:agents": "node scripts/gen-agents.js" }, "dependencies": { 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/bracket-left.svg b/apps/web/public/icons/bracket-left.svg new file mode 100644 index 0000000000..4e8e1bfe0b --- /dev/null +++ b/apps/web/public/icons/bracket-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/icons/bracket-right.svg b/apps/web/public/icons/bracket-right.svg new file mode 100644 index 0000000000..4dad56d8d8 --- /dev/null +++ b/apps/web/public/icons/bracket-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/icons/claude.svg b/apps/web/public/icons/claude.svg new file mode 100644 index 0000000000..879ad81261 --- /dev/null +++ b/apps/web/public/icons/claude.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/public/icons/cursor.svg b/apps/web/public/icons/cursor.svg new file mode 100644 index 0000000000..383af65661 --- /dev/null +++ b/apps/web/public/icons/cursor.svg @@ -0,0 +1,6 @@ + + + diff --git a/apps/web/scripts/build-content-collections.js b/apps/web/scripts/build-content-collections.js new file mode 100644 index 0000000000..198db12091 --- /dev/null +++ b/apps/web/scripts/build-content-collections.js @@ -0,0 +1,27 @@ +import { + createBuilder, + suppressDeprecatedWarnings, +} from "@content-collections/core"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const configPath = join(__dirname, "../content-collections.ts"); + +// Keep typecheck output focused on actual failures until the deprecated +// content-collections config shape is migrated separately. +suppressDeprecatedWarnings( + "collectionsConfigProperty", + "implicitContentProperty", +); + +async function main() { + const builder = await createBuilder(configPath); + await builder.build(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/apps/web/src/components/daily-note-mock.tsx b/apps/web/src/components/daily-note-mock.tsx new file mode 100644 index 0000000000..25ba330ab8 --- /dev/null +++ b/apps/web/src/components/daily-note-mock.tsx @@ -0,0 +1,172 @@ +import { Icon } from "@iconify-icon/react"; + +import { cn } from "@hypr/utils"; + +export function DailyNoteMock({ className }: { className?: string }) { + return ( +
+
+
+
+
+
+
+ +
+ +
+ +
+
+ + Team Standup +
+
+ + Design review +
+
+ +
+ +
+ + +
+ JD +
+
+
+ +
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ ); +} + +function DayHeader({ + label, + today, + muted, +}: { + label: string; + today?: boolean; + muted?: boolean; +}) { + return ( +
+

+ {label} +

+ {today && ( + + today + + )} +
+ ); +} + +function DaySeparator() { + return
; +} + +function MeetingTaskRow({ + title, + time, + done, +}: { + title: string; + time: string; + done?: boolean; +}) { + return ( +
+ + + + {title} + + + {time} + +
+ ); +} + +function ActionTaskRow({ label, due }: { label: string; due?: boolean }) { + return ( +
+ + {label} + {due && ( + + Due + + )} +
+ ); +} + +function TaskCheckbox({ done }: { done?: boolean }) { + return ( + + {done && } + + ); +} diff --git a/apps/web/src/components/download-button.tsx b/apps/web/src/components/download-button.tsx index 85eaf8dcd2..1890cb4a62 100644 --- a/apps/web/src/components/download-button.tsx +++ b/apps/web/src/components/download-button.tsx @@ -82,7 +82,7 @@ export function DownloadButton({ download onClick={handleClick} className={cn([ - "group relative flex h-9 items-center justify-center overflow-hidden px-5 text-sm", + "group relative flex h-9 items-center justify-center overflow-hidden px-6 text-sm", "surface-dark rounded-full text-white", ])} > @@ -109,7 +109,7 @@ export function DownloadButton({ download onClick={handleClick} className={cn([ - "group relative flex h-14 items-center justify-center overflow-hidden pr-8 pl-4", + "group relative flex h-14 items-center justify-center overflow-hidden pr-8 pl-8", "surface-dark rounded-full text-white", ])} > diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 89e3cbc1a5..1866247792 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -45,6 +45,7 @@ function getMaxWidthClass(pathname: string): string { } const featuresList = [ + { to: "/product/daily-notes", label: "Daily Notes" }, { to: "/product/ai-notetaking", label: "AI Notetaking" }, { to: "/product/search", label: "Searchable Notes" }, { to: "/gallery?type=template", label: "Custom Templates" }, diff --git a/apps/web/src/components/jira-workflow-mock.tsx b/apps/web/src/components/jira-workflow-mock.tsx new file mode 100644 index 0000000000..36df0eb107 --- /dev/null +++ b/apps/web/src/components/jira-workflow-mock.tsx @@ -0,0 +1,101 @@ +import { Icon } from "@iconify-icon/react"; +import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useState } from "react"; + +import { JiraToolCall } from "./ai-feature-panel"; + +const CYCLE_MS = 6500; + +export function JiraWorkflowMock() { + const [step, setStep] = useState(0); + const [loopKey, setLoopKey] = useState(0); + + useEffect(() => { + setStep(0); + const t1 = setTimeout(() => setStep(1), 400); + const t2 = setTimeout(() => setStep(2), 1200); + const t3 = setTimeout(() => setStep(3), 3800); + const restart = setTimeout(() => setLoopKey((k) => k + 1), CYCLE_MS); + return () => { + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + clearTimeout(restart); + }; + }, [loopKey]); + + return ( +
+
+
+ + Chat +
+
+ +
+ + {step >= 1 && ( + +
+

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

+
+
+ )} + {step >= 2 && ( + + + + )} + {step >= 3 && ( + + +

+ Jira ticket ENG-247 created and assigned to Sarah. +

+
+ )} +
+
+ +
+
+ + Ask about your notes... + +
+ Send +
+
+
+
+ ); +} diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index d5b5038d9f..a30c253847 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -41,6 +41,7 @@ type MenuGroup = { }; const featuresList: MenuItem[] = [ + { to: "/product/daily-notes", label: "Daily Notes" }, { to: "/product/ai-notetaking", label: "AI Notetaking" }, { to: "/product/search", label: "Searchable Notes" }, { to: "/gallery?type=template", label: "Custom Templates" }, diff --git a/apps/web/src/components/stickers.tsx b/apps/web/src/components/stickers.tsx new file mode 100644 index 0000000000..a072778062 --- /dev/null +++ b/apps/web/src/components/stickers.tsx @@ -0,0 +1,83 @@ +import { cn } from "@hypr/utils"; + +import { + GITHUB_LAST_SEEN_STARS, + GITHUB_ORG_REPO, + useGitHubStats, +} from "../queries"; + +export function YCombinatorSticker({ className }: { className?: string }) { + return ( + + ); +} + +function formatStars(n: number) { + return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`; +} + +export function GitHubStarsSticker({ className }: { className?: string }) { + const githubStats = useGitHubStats(); + const starCount = githubStats.data?.stars ?? GITHUB_LAST_SEEN_STARS; + + return ( + + ); +} diff --git a/apps/web/src/components/timeline-recall-mock.tsx b/apps/web/src/components/timeline-recall-mock.tsx new file mode 100644 index 0000000000..53864cc04c --- /dev/null +++ b/apps/web/src/components/timeline-recall-mock.tsx @@ -0,0 +1,83 @@ +import { motion } from "motion/react"; + +const timelineBlocks = [ + { + time: "10:00 – 11:30 AM", + title: "Finances & Admin", + body: "Checked Mercury banking and PostHog analytics, then shifted to tax and payroll admin.", + bullets: [ + "Pulled contractor documents from Gusto", + "Drafted W-9 request emails from Mail", + ], + }, + { + time: "11:50 AM – 12:25 PM", + title: 'Writing "Moats Suck"', + body: "Drafted an essay in Obsidian, then cross-published to LinkedIn and X.", + bullets: ["~600–780 words", "Shared in #general and #uchar"], + }, + { + time: "1:00 – 3:30 PM", + title: "Coding — Char v0.0.8", + body: "Updated Char and configured transcription settings.", + bullets: [ + "Added created_at timestamp display", + "Explored Soniqo Docs for speech enhancement", + ], + }, + { + time: "3:30 – 4:30 PM", + title: "Discord & Team", + body: "Active in the CEO's Office channel, directing the AI agent.", + bullets: ["Reviewed SQLite migration work", "Triaged GitHub issue #5047"], + }, +]; + +export function TimelineRecallMock() { + return ( +
+
+
+ + + {[...timelineBlocks, ...timelineBlocks].map((block, i) => ( + + ))} + +
+ ); +} + +function TimelineBlock({ + time, + title, + body, + bullets, +}: { + time: string; + title: string; + body: string; + bullets: string[]; +}) { + return ( +
+
+ {time} +
+

{title}

+

{body}

+
    + {bullets.map((b) => ( +
  • + + {b} +
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 54ce54be88..3355d4cbe4 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -83,6 +83,7 @@ import { Route as ViewProductLocalAiRouteImport } from './routes/_view/product/l import { Route as ViewProductIntegrationsRouteImport } from './routes/_view/product/integrations' import { Route as ViewProductFlexibleAiRouteImport } from './routes/_view/product/flexible-ai' import { Route as ViewProductExtensionsRouteImport } from './routes/_view/product/extensions' +import { Route as ViewProductDailyNotesRouteImport } from './routes/_view/product/daily-notes' import { Route as ViewProductBotRouteImport } from './routes/_view/product/bot' import { Route as ViewProductApiRouteImport } from './routes/_view/product/api' import { Route as ViewProductAiNotetakingRouteImport } from './routes/_view/product/ai-notetaking' @@ -521,6 +522,11 @@ const ViewProductExtensionsRoute = ViewProductExtensionsRouteImport.update({ path: '/product/extensions', getParentRoute: () => ViewRouteRoute, } as any) +const ViewProductDailyNotesRoute = ViewProductDailyNotesRouteImport.update({ + id: '/product/daily-notes', + path: '/product/daily-notes', + getParentRoute: () => ViewRouteRoute, +} as any) const ViewProductBotRoute = ViewProductBotRouteImport.update({ id: '/product/bot', path: '/product/bot', @@ -924,6 +930,7 @@ export interface FileRoutesByFullPath { '/product/ai-notetaking': typeof ViewProductAiNotetakingRoute '/product/api': typeof ViewProductApiRoute '/product/bot': typeof ViewProductBotRoute + '/product/daily-notes': typeof ViewProductDailyNotesRoute '/product/extensions': typeof ViewProductExtensionsRoute '/product/flexible-ai': typeof ViewProductFlexibleAiRoute '/product/integrations': typeof ViewProductIntegrationsRoute @@ -1059,6 +1066,7 @@ export interface FileRoutesByTo { '/product/ai-notetaking': typeof ViewProductAiNotetakingRoute '/product/api': typeof ViewProductApiRoute '/product/bot': typeof ViewProductBotRoute + '/product/daily-notes': typeof ViewProductDailyNotesRoute '/product/extensions': typeof ViewProductExtensionsRoute '/product/flexible-ai': typeof ViewProductFlexibleAiRoute '/product/integrations': typeof ViewProductIntegrationsRoute @@ -1201,6 +1209,7 @@ export interface FileRoutesById { '/_view/product/ai-notetaking': typeof ViewProductAiNotetakingRoute '/_view/product/api': typeof ViewProductApiRoute '/_view/product/bot': typeof ViewProductBotRoute + '/_view/product/daily-notes': typeof ViewProductDailyNotesRoute '/_view/product/extensions': typeof ViewProductExtensionsRoute '/_view/product/flexible-ai': typeof ViewProductFlexibleAiRoute '/_view/product/integrations': typeof ViewProductIntegrationsRoute @@ -1343,6 +1352,7 @@ export interface FileRouteTypes { | '/product/ai-notetaking' | '/product/api' | '/product/bot' + | '/product/daily-notes' | '/product/extensions' | '/product/flexible-ai' | '/product/integrations' @@ -1478,6 +1488,7 @@ export interface FileRouteTypes { | '/product/ai-notetaking' | '/product/api' | '/product/bot' + | '/product/daily-notes' | '/product/extensions' | '/product/flexible-ai' | '/product/integrations' @@ -1619,6 +1630,7 @@ export interface FileRouteTypes { | '/_view/product/ai-notetaking' | '/_view/product/api' | '/_view/product/bot' + | '/_view/product/daily-notes' | '/_view/product/extensions' | '/_view/product/flexible-ai' | '/_view/product/integrations' @@ -2275,6 +2287,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ViewProductExtensionsRouteImport parentRoute: typeof ViewRouteRoute } + '/_view/product/daily-notes': { + id: '/_view/product/daily-notes' + path: '/product/daily-notes' + fullPath: '/product/daily-notes' + preLoaderRoute: typeof ViewProductDailyNotesRouteImport + parentRoute: typeof ViewRouteRoute + } '/_view/product/bot': { id: '/_view/product/bot' path: '/product/bot' @@ -2833,6 +2852,7 @@ interface ViewRouteRouteChildren { ViewProductAiNotetakingRoute: typeof ViewProductAiNotetakingRoute ViewProductApiRoute: typeof ViewProductApiRoute ViewProductBotRoute: typeof ViewProductBotRoute + ViewProductDailyNotesRoute: typeof ViewProductDailyNotesRoute ViewProductExtensionsRoute: typeof ViewProductExtensionsRoute ViewProductFlexibleAiRoute: typeof ViewProductFlexibleAiRoute ViewProductIntegrationsRoute: typeof ViewProductIntegrationsRoute @@ -2903,6 +2923,7 @@ const ViewRouteRouteChildren: ViewRouteRouteChildren = { ViewProductAiNotetakingRoute: ViewProductAiNotetakingRoute, ViewProductApiRoute: ViewProductApiRoute, ViewProductBotRoute: ViewProductBotRoute, + ViewProductDailyNotesRoute: ViewProductDailyNotesRoute, ViewProductExtensionsRoute: ViewProductExtensionsRoute, ViewProductFlexibleAiRoute: ViewProductFlexibleAiRoute, ViewProductIntegrationsRoute: ViewProductIntegrationsRoute, diff --git a/apps/web/src/routes/_view/index.tsx b/apps/web/src/routes/_view/index.tsx index d0adaa7d5e..d00cd42710 100644 --- a/apps/web/src/routes/_view/index.tsx +++ b/apps/web/src/routes/_view/index.tsx @@ -4,28 +4,41 @@ 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 { VideoIcon } from "lucide-react"; -import { AnimatePresence, motion, useInView } from "motion/react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { Mail, PencilLine, VideoIcon, SquareCheck } from "lucide-react"; +import { + AnimatePresence, + motion, + useInView, + useMotionValue, + useMotionValueEvent, + useScroll, + useTransform, +} from "motion/react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { cn } from "@hypr/utils"; -import { AcquisitionLinkGrid } from "@/components/acquisition-link-grid"; import { ContactSearchToolCall, TranscriptToolCall, } from "@/components/ai-feature-panel"; -import { AppPreviewSection } from "@/components/app-preview"; import { CTASection } from "@/components/cta-section"; +import { DailyNoteMock } from "@/components/daily-note-mock"; import { DownloadButton } from "@/components/download-button"; import { GitHubOpenSource } from "@/components/github-open-source"; -import { GithubStars } from "@/components/github-stars"; import { Image } from "@/components/image"; +import { JiraWorkflowMock } from "@/components/jira-workflow-mock"; import { LogoCloud } from "@/components/logo-cloud"; import { FAQ, FAQItem } from "@/components/mdx-shared"; -import { NotebookGrid } from "@/components/notebook-grid"; import { SocialCard } from "@/components/social-card"; -import { VideoModal } from "@/components/video-modal"; +import { GitHubStarsSticker, YCombinatorSticker } from "@/components/stickers"; +import { TimelineRecallMock } from "@/components/timeline-recall-mock"; import { addContact } from "@/functions/loops"; import { useHeroContext } from "@/hooks/use-hero-context"; import { getHeroCTA, usePlatform } from "@/hooks/use-platform"; @@ -40,8 +53,6 @@ import { import { AnnouncementBanner } from "./route"; -const MUX_PLAYBACK_ID = "1s01BC9LBwzygOUWk9Pdn011KuxvIQRMbTEfCpOypfdrw"; - const mainFeatures = [ { icon: "mdi:text-box-outline", @@ -133,43 +144,32 @@ function useHasEnteredView( } function Component() { - const [expandedVideo, setExpandedVideo] = useState(null); const heroInputRef = useRef(null); return (
- + - - - - + + + + + -
- setExpandedVideo(null)} - />
); } function HeroSection({ - onVideoExpand, heroInputRef, }: { - onVideoExpand: (id: string) => void; heroInputRef: React.RefObject; }) { const platform = usePlatform(); @@ -243,21 +243,65 @@ function HeroSection({ id="hero" className="isolate flex w-full overflow-visible pt-10 text-left" > -
-
-
+
+
+

- Meeting Notes
You Own + AI daily notes
that remember and act

-

- Char captures every meeting without a bot and keeps data on - your device. +

+ Char records your meetings without bots, pulls action items + from your{" "} + + {" "} + emails + + , and builds a{" "} + + {" "} + daily note + {" "} + with everything you need to do. +

+

+ You review it,{" "} + + {" "} + tick what's done + + , and hand off the rest to AI agents like{" "} + + + Claude + {" "} + or{" "} + + + Cursor + + .

{heroCTA.showInput ? (
-
)}
-
- +
+
+ + - - - - - -
- Backed by - - Y Combinator - -
-
- -
-
- - +
+
@@ -1819,133 +1811,6 @@ export function AISection() { ); } -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 - - -
- -
-
-
-
-
-
- S -
-
-

Sarah Chen

-

- Product Lead · Acme Inc -

-
-
-
- sarah@acme.com · +1 (415) 555-0123 -
-
-

- Last conversation -

-

- Discussed Q2 roadmap priorities and timeline for the mobile - redesign. Agreed to share updated specs by Friday. -

-
-
-
-
-

- Have your contacts in one place -

-

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

-
    -
  • - - All your chats linked - -
  • -
  • - - Generate summaries from meetings - -
  • -
-
-
-
-
-
-
- -
-

- Weekly Team Sync -

-

- Starting in 2 min -

-
-
- -
-
-
-

- Work with your calendar -

-

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

-
    -
  • - - Automatic meeting linking - -
  • -
  • - - Pre-meeting context and preparation - -
  • -
  • - - Timeline view with notes - -
  • -
-
-
-
-
-
- ); -} - export function MainFeaturesSection({ featuresScrollRef, selectedFeature, @@ -2481,16 +2346,16 @@ const solutionColors: Record< string, { accent: string; bg: string; border: string } > = { + founders: { + accent: "oklch(0.62 0.17 42)", + bg: "#fff7ed", + border: "#fed7aa", + }, developers: { accent: "oklch(0.55 0.2245 292.58)", bg: "#f5f3ff", border: "#ddd6fe", }, - enterprise: { - accent: "#374151", - bg: "#f9fafb", - border: "#d1d5db", - }, research: { accent: "oklch(0.5471 0.1899 264.38)", bg: "#eff6ff", @@ -2499,6 +2364,21 @@ const solutionColors: Record< }; const solutionScenarios = [ + { + id: "founders", + label: "Founders", + headline: "One source of truth for every conversation you run", + description: + "Investor calls, customer discovery, team 1:1s, board updates. Char captures it all with zero overhead and keeps the context you need to act.", + pills: [ + "Multiple agendas", + "Investor & customer memory", + "Cross-context handoff", + "Agent-ready workflows", + "Your data, your rules", + ], + link: "/solution/founders", + }, { id: "developers", label: "Developers", @@ -2514,21 +2394,6 @@ const solutionScenarios = [ ], link: "/solution/engineering", }, - { - id: "enterprise", - label: "Enterprise", - headline: "Meeting AI configured for your organization", - description: - "Other AI note-takers ask you to trust their infrastructure, their models, and their policies. We built one where you control all three.", - pills: [ - "Self-Hosted Deployment", - "Zero-Knowledge Security", - "Compliance Ready", - "Access Control", - "Open Source", - ], - link: "/enterprise", - }, { id: "research", label: "Research", @@ -2679,60 +2544,6 @@ function SolutionsTabbar() { ); } -function ExplorePathsSection() { - return ( - - ); -} - function FAQSection() { return (
@@ -2868,3 +2679,594 @@ function BlogSection() {
); } + +type ScrollEffect = "opacity" | "blur" | "blurUp"; + +function ScrollRevealWord({ + progress, + range, + effect, + children, +}: { + progress: ReturnType>; + range: [number, number]; + effect: ScrollEffect; + children: React.ReactNode; +}) { + 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]); + + return {children}; +} + +function ScrollRevealParagraph({ + children, + effect = "blur", + className, +}: { + children: React.ReactNode; + effect?: ScrollEffect; + className?: string; +}) { + const containerRef = useRef(null); + const { scrollYProgress } = useScroll({ + target: containerRef, + offset: ["start 0.8", "end 0.8"], + }); + + 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<{ + children?: React.ReactNode; + }>; + 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<{ + children?: React.ReactNode; + style?: React.CSSProperties; + }>; + + 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 ManifestoSection() { + const paragraphClass = + "font-sans text-xl leading-relaxed text-fg md:text-3xl lg:text-3xl"; + const paragraphSpacing = "mt-4 md:mt-6 lg:mt-8"; + 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 +
+ +
+

+ Char +

+

John Jeong, Yujong Lee

+
+ +
+ Char Signature +
+
+
+
+ ); +} + +function RecordYourDaySection() { + return ( +
+

+ This is how Char gets your day done +

+
+
+

+ Screen recording +

+

+ Record your day +

+

+ Char remembers your timeline and helps you get any information you + want: emails, tasks, conversations. +

+

+ — Ask Charlie anything, about day, contacts, meetings. +

+
+ +
+ +
+
+
+ ); +} + +function SummariseMeetingsSection() { + return ( +
+
+
+

+ Collect meetings +

+

+ Summarise and transcribe meetings +

+

+ Char combines your meeting notes with transcripts to create a + perfect summary. +

+ + Learn more about meeting notetaker + + +
+ +
+ + +
+
+
+ ); +} + +function SummaryPreview() { + return ( +
+
+
+
+
+
+
+
+ + Char + +
+
+
+
+

+ Mobile UI Update and API Adjustments +

+
    +
  • + Sarah presented the new mobile UI update, which includes a + streamlined navigation bar and improved button placements for + better accessibility. +
  • +
  • + Ben confirmed that API adjustments are needed to support dynamic + UI changes, particularly for fetching personalized user data more + efficiently. +
  • +
+
+
+

+ New Dashboard – Urgent Priority +

+
    +
  • + Alice emphasized that the new analytics dashboard must be + prioritized due to increasing stakeholder demand. +
  • +
+
+
+
+ ); +} + +function ExecuteTasksSection() { + return ( +
+
+
+

+ AI chat +

+

+ Ask questions, execute tasks +

+

+ Chat with Charlie to find decisions, pull context, and trigger the + follow-ups that usually slip — all grounded in your notes. +

+

+ — Hand off work to AI agents like Claude or Cursor. +

+
+ +
+ +
+
+
+ ); +} + +function PrivateByDesignSection() { + return ( +
+
+
+

+ Private by design +

+
+ +
+
+
+ + + + + +
+
+

+ Data always stays on your device +

+

+ Your privacy is our priority. We don't use it for training or + collecting any of your meeting content. +

+
+
+ +
+
+
+ + sk- + + + ✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱✱ + +
+
+
+
+

+ Bring Your Own Key or use local models +

+

+ Char supports all major speech-to-text providers and have the + best local models build in +

+
+
+ +
+
+
+
+ +
+
+ + 1-1 w/ Janice + + + 3 participants + +
+
+
+
+

+ No bots on calls. Hidden during screen share. +

+

+ Char captures system audio, not bothers people on the call. + Works everywhere. +

+
+
+ +
+
+
+ +
+ + Screen activity + + + Processed locally + +
+
+
+
+

+ Screen recording stays on your device +

+

+ We record and analyse your screen locally. It never goes to the + cloud. +

+
+
+
+
+
+ ); +} diff --git a/apps/web/src/routes/_view/product/ai-notetaking.tsx b/apps/web/src/routes/_view/product/ai-notetaking.tsx index bd742387e1..babbacd952 100644 --- a/apps/web/src/routes/_view/product/ai-notetaking.tsx +++ b/apps/web/src/routes/_view/product/ai-notetaking.tsx @@ -12,7 +12,7 @@ import { CTASection as SharedCTA } from "@/components/cta-section"; import { DownloadButton } from "@/components/download-button"; import { GithubStars } from "@/components/github-stars"; import { Image } from "@/components/image"; -import { HowItWorksSection } from "@/routes/_view/index"; +import { AISection, HowItWorksSection } from "@/routes/_view/index"; export const Route = createFileRoute("/_view/product/ai-notetaking")({ component: Component, @@ -46,6 +46,7 @@ function Component() {
+ diff --git a/apps/web/src/routes/_view/product/daily-notes.tsx b/apps/web/src/routes/_view/product/daily-notes.tsx new file mode 100644 index 0000000000..fd63e06794 --- /dev/null +++ b/apps/web/src/routes/_view/product/daily-notes.tsx @@ -0,0 +1,43 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { DownloadButton } from "@/components/download-button"; +import { GithubStars } from "@/components/github-stars"; + +export const Route = createFileRoute("/_view/product/daily-notes")({ + component: Component, + head: () => ({ + meta: [ + { title: "Daily Notes - Char" }, + { + name: "description", + content: + "Char builds a daily note from your meetings, emails, and screen activity. Review what happened, tick what's done, and hand off the rest to AI agents.", + }, + { name: "robots", content: "noindex, nofollow" }, + ], + }), +}); + +function Component() { + return ( +
+
+
+

+ Your day, already written. +

+

+ Char records meetings without bots, pulls action items from your + emails, and builds a daily note with everything you need to do. You + review it, tick what's done, and hand off the rest to AI agents like + Claude or Cursor. +

+
+ + +
+
+
+
+ ); +} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index f1137e08c5..e96d636170 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -80,8 +80,8 @@ /* ── Colors: greys ────────────────────────────── */ --grey-900: oklch(0.3 0.0197 81.53); --grey-700: oklch(0.4922 0.0127 67.79); - --grey-600: oklch(0.6182 0.0018 67.8); - --grey-500: oklch(0.7782 0.0018 67.8); + --grey-600: oklch(0.6933 0.0018 68.29); + --grey-500: oklch(0.7485 0.0025 67.8); --grey-300: oklch(0.9213 0.0027 106.45); --grey-100: oklch(0.9558 0.0045 78.3); @@ -98,7 +98,7 @@ /* ── Colors: borders ─────────────────────────── */ --color-border: var(--grey-500); --color-border-subtle: var(--grey-300); - --color-border-bright: oklch(0.5959 0.0333 78.6); + --color-border-bright: var(--grey-600); --color-border-active: oklch(0.9213 0.0027 106.45); /* ── Colors: brand ───────────────────────────── */ @@ -402,6 +402,16 @@ ); } +.bg-lined-notebook-bright { + background-image: repeating-linear-gradient( + to bottom, + transparent, + transparent 32px, + var(--color-border) 32px, + var(--color-border) 33px + ); +} + .bg-dotted { background-image: radial-gradient( circle, diff --git a/package.json b/package.json index b7b97dabc0..c145dc40d7 100644 --- a/package.json +++ b/package.json @@ -44,16 +44,8 @@ "@tiptap/extension-floating-menu": "3.20.1", "@tiptap/extensions": "3.20.1", "prosemirror-view": "1.41.7", - "lightningcss": "1.30.1" - }, - "peerDependencyRules": { - "allowedVersions": { - "motion": "11", - "react": "19", - "@lobehub/ui": "3.4.0", - "unist-util-visit": "5", - "esbuild": "0.25" - } + "lightningcss": "1.30.1", + "@astrojs/internal-helpers": "0.8.0" } }, "packageManager": "pnpm@10.33.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85d724c934..a4d3a3bd0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: '@tiptap/extension-paragraph': 3.20.1 + '@astrojs/internal-helpers': 0.8.0 '@tiptap/extension-blockquote': 3.20.1 '@tiptap/extension-bold': 3.20.1 '@tiptap/extension-code': 3.20.1