diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 15d40858b3..5604e07df0 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -8,3 +8,70 @@ infisical export \ --projectId=87dad7b5-72a6-4791-9228-b3b86b169db1 \ --path="/web" ``` + +## Design System + +All visual tokens live in `src/styles.css` inside the `@theme` block. Never use hardcoded hex values in components — always reference a token. + +### Color tokens + +| Token | Value | Use for | +|---|---|---| +| `--color-page` | `#F2F3F4` | Page/canvas background (`bg-page`) | +| `--color-surface` | `#ffffff` | Card, panel, modal backgrounds (`bg-surface`) | +| `--color-surface-subtle` | `#fafaf9` | Muted surface variants (`bg-surface-subtle`) | +| `--color-fg` | `#1c1917` | Primary text (`text-fg`) | +| `--color-fg-muted` | `#57534e` | Secondary/body text (`text-fg-muted`) | +| `--color-fg-subtle` | `#a8a29e` | Placeholder, disabled, icons (`text-fg-subtle`) | +| `--color-border` | `#CBC8BD` | Default borders, dividers (`border-border`) | +| `--color-border-subtle` | `#f5f5f5` | Hairline/structural borders (`border-border-subtle`) | +| `--color-brand` | `#78716c` | Brand accent — use sparingly (`bg-brand`, `text-brand`) | +| `--color-brand-dark` | `#57534e` | CTA button gradient end, emphasis (`bg-brand-dark`) | + +### Shadow tokens + +| Token | Use for | +|---|---| +| `--shadow-ring` | 1px outline border effect — prefer over `border` when stacking borders | +| `--shadow-ring-subtle` | Same, using the subtle border color | + +The `.border-shadow` utility class applies `--shadow-ring` as a convenience. + +### Typography + +- **Display / logo**: `font-serif` (Fraunces) — headings, logo wordmark, editorial emphasis +- **Body / UI text**: `font-sans` (Geist) — all body copy, labels, nav links +- **Code / buttons**: `font-mono` (Geist Mono) — code, button labels, monospaced UI + +### CTA button pattern + +Primary CTA always uses the warm gradient: + +```tsx +"bg-linear-to-t from-brand-dark to-brand rounded-full text-white" +``` + +Secondary / ghost uses surface + border: + +```tsx +"bg-surface border border-border rounded-lg text-fg-muted" +``` + +## Component structure + +Target folder layout. New components must go in the right folder — do not add to the flat root of `components/`. + +``` +src/components/ + layout/ # Header, Footer, Sidebar, SidebarNavigation + ui/ # Primitive brand components: Button, Badge, Card shells + sections/ # Page-level marketing sections (LogoCloud, CtaCard, GithubStars…) + mdx/ # MDX renderer components + notepad/ # Notepad feature + transcription/ # Transcription feature + admin/ # Admin tooling +``` + +Existing flat-root files are being migrated incrementally. When touching a file, move it to the right folder as part of that PR. + +For full brand reference, see `BRAND.md`. diff --git a/apps/web/BRAND.md b/apps/web/BRAND.md new file mode 100644 index 0000000000..18a49f4dc9 --- /dev/null +++ b/apps/web/BRAND.md @@ -0,0 +1,180 @@ +# Char Brand + +This document is the single source of truth for Char's visual identity on the web. All tokens referenced here are defined in `src/styles.css` and available as Tailwind utilities. + +--- + +## Color + +The palette is warm neutral — rooted in stone and off-white. One moment of warmth (the brand gradient on CTAs). Everything else recedes. + +### Palette + +| Role | Token | Hex | Tailwind class | +|---|---|---|---| +| Page background | `--color-page` | `#F2F3F4` | `bg-page` | +| Surface | `--color-surface` | `#ffffff` | `bg-surface` | +| Surface muted | `--color-surface-subtle` | `#fafaf9` | `bg-surface-subtle` | +| Primary text | `--color-fg` | `#1c1917` | `text-fg` | +| Secondary text | `--color-fg-muted` | `#57534e` | `text-fg-muted` | +| Placeholder / disabled | `--color-fg-subtle` | `#a8a29e` | `text-fg-subtle` | +| Default border | `--color-border` | `#CBC8BD` | `border-border` | +| Hairline border | `--color-border-subtle` | `#f5f5f5` | `border-border-subtle` | +| Brand accent | `--color-brand` | `#78716c` | `bg-brand` / `text-brand` | +| Brand dark | `--color-brand-dark` | `#57534e` | `bg-brand-dark` | + +### Usage rules + +- Never introduce a color outside this palette without updating the token set first. +- `color-brand` and `color-brand-dark` are the only "warm accent" moments. Use them exclusively for primary CTAs and interactive focus states. +- Tailwind's `neutral-*` and `stone-*` scales are available as fallback but semantic tokens above take precedence for any brand-facing UI. + +--- + +## Typography + +Three typefaces, each with a distinct role. Do not mix roles. + +| Face | Font | Variable | Role | +|---|---|---|---| +| Serif | Fraunces | `--font-serif` | Wordmark, editorial pull-quotes | +| Sans | Geist | `--font-sans` | All body copy, UI labels, navigation | +| Mono | Geist Mono | `--font-mono` | Button labels, display headings, code, technical UI | + +### Type scale principles + +- **Heading hierarchy**: h1/h2 use `font-mono` by default (per base styles). Serif is used selectively for editorial or brand moments, not for all headings. +- **Weight contrast**: Pair heavy display weight (`font-semibold` / `font-bold`) with light body weight (`font-normal`). Never use two heavy weights adjacently. +- **Letter spacing**: Tight (`tracking-tight`) on large display type. Open (`tracking-wider`) on all-caps labels and category tags. +- **Minimum readable size**: 14px for body, 12px only for all-caps labels or metadata. + +--- + +## Borders & Shadows + +| Token | Value | Class | Use | +|---|---|---|---| +| `--shadow-ring` | `0 0 0 1px #CBC8BD` | `.border-shadow` | Default card/panel outline | +| `--shadow-ring-subtle` | `0 0 0 1px #f5f5f5` | — | Hairline structural outlines | + +**Prefer `shadow-ring` over CSS `border`** when an element already has box-shadow — avoids double-border stacking issues. + +Border radius conventions: +- `rounded-xs` — tight UI elements (chips, small badges) +- `rounded-md` — cards, inputs, dropdowns +- `rounded-lg` — modals, large cards +- `rounded-full` — pill buttons, avatars, tags + +--- + +## Spacing + +The layout uses a base-4 spacing rhythm. Key structural values: + +| Purpose | Value | +|---|---| +| Header height | `69px` / `h-17.25` | +| Content max-width | `max-w-6xl` | +| Wide breakpoint | `72rem` (`laptop`) | +| Section vertical padding | `py-12` (mobile) / `py-16` (desktop) | +| Card internal padding | `p-4` (compact) / `p-8` (feature) | + +--- + +## Components + +### Primary CTA button + +The main action. Warm gradient, pill shape, scales on hover. + +```tsx + +``` + +### Secondary / ghost button + +Outline style, no fill. Used for secondary actions. + +```tsx + +``` + +### Nav link + +Text-only, dotted underline on hover. + +```tsx + + Link + +``` + +### Section label (category tag) + +All-caps mono, wide tracking, muted. + +```tsx + + Features + +``` + +### Card + +No heavy shadow. Border ring or hairline border, surface background. + +```tsx +
+ … +
+``` + +Or with `shadow-ring` instead of `border`: + +```tsx +
+ … +
+``` + +--- + +## Logo + +- **Wordmark**: "Char" in `font-serif` (Fraunces), `text-2xl`, `font-semibold`. +- **Do not** render the wordmark in `font-sans` or `font-mono`. +- **Scale animation** on hover: `hover:scale-105 transition-transform` is the only permitted motion on the logo. + +--- + +## Motion + +- Scale micro-interactions: `hover:scale-[102%] active:scale-[98%]` — used on all interactive cards and CTA buttons. +- Opacity transitions: `transition-opacity duration-200` — used for fade in/out on dynamic text. +- Page-level slide-in: `animate-in slide-in-from-top duration-300` — used for mobile menu only. +- No bounce, no spring, no decorative keyframes on brand UI. + +--- + +## Component folder structure + +``` +src/components/ + layout/ # Header, Footer, Sidebar — structural chrome + ui/ # Primitive, stateless brand components (Button, Badge, Card) + sections/ # Composed page sections (LogoCloud, CtaCard, GithubStars) + mdx/ # MDX renderer overrides + notepad/ # Notepad product feature + transcription/ # Transcription product feature + admin/ # Internal admin tooling +``` + +When creating a new component, ask: +- Is it a stateless visual primitive? → `ui/` +- Is it a full page section? → `sections/` +- Is it structural layout chrome? → `layout/` +- Is it tied to a specific product feature? → that feature's folder diff --git a/apps/web/package.json b/apps/web/package.json index b0bd85e42b..c7a30871e1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "dev": "VITE_APP_URL=\"http://localhost:3000\" VITE_API_URL=\"http://localhost:3001\" dotenvx run --ignore MISSING_ENV_FILE -f ../../.env.supabase -f .env -- vite dev --port 3000", - "build": "vite build && cp public/sitemap.xml dist/client/sitemap.xml && pagefind --site ./dist/client", + "dev": "NODE_OPTIONS='--max-old-space-size=8192' VITE_APP_URL=\"http://localhost:3000\" VITE_API_URL=\"http://localhost:3001\" dotenvx run --ignore MISSING_ENV_FILE -f ../../.env.supabase -f .env -- vite dev --port 3000", + "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": "playwright test", "typecheck": "CI=true pnpm -F @hypr/web build && tsc --project tsconfig.json --noEmit", diff --git a/apps/web/src/components/ai-feature-panel.tsx b/apps/web/src/components/ai-feature-panel.tsx new file mode 100644 index 0000000000..3aaa3c1a8b --- /dev/null +++ b/apps/web/src/components/ai-feature-panel.tsx @@ -0,0 +1,201 @@ +import { Icon } from "@iconify-icon/react"; +import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useState } from "react"; + +import { cn } from "@hypr/utils"; + +export function SearchToolCall({ loopKey }: { loopKey: 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); + }; + }, [loopKey]); + + 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) => ( + + + {m} + + ))} + + )} + +
+ ); +} + +export function JiraToolCall({ loopKey }: { loopKey: 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); + }; + }, [loopKey]); + + return ( +
+
+ + + {phase < 1 ? ( + + + Creating ticket... + + ) : ( + + ENG-247 + + Created + + + )} + +
+ + {phase >= 2 && ( + +

+ Mobile UI bug fix +

+
+ )} +
+ + {phase >= 3 && ( + +
+ S +
+ Sarah +
+ )} +
+
+ ); +} + +export function TranscriptToolCall({ loopKey }: { loopKey: 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); + }; + }, [loopKey]); + + 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... +
+ )} +
+
+ ); +} diff --git a/apps/web/src/components/mdx-jobs.tsx b/apps/web/src/components/mdx-jobs.tsx index 22684efe35..c10cfb5b27 100644 --- a/apps/web/src/components/mdx-jobs.tsx +++ b/apps/web/src/components/mdx-jobs.tsx @@ -287,7 +287,7 @@ export function FAQItem({ export function FAQ({ children }: { children: React.ReactNode }) { return ( -
+
{children}
); 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..81cc5cfc51 --- /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 29a469d9d5..79fc604fd5 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/components/notebook-grid.tsx b/apps/web/src/components/notebook-grid.tsx new file mode 100644 index 0000000000..bd795fa51e --- /dev/null +++ b/apps/web/src/components/notebook-grid.tsx @@ -0,0 +1,109 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +const TARGET_CELL_SIZE = 56; +const ROTATIONS = [0, 90, 180, 270] as const; + +function CellPattern() { + return ( + + {[39, 32, 25, 25, 17].map((r, i) => ( + + ))} + + ); +} + +export function NotebookGrid() { + const containerRef = useRef(null); + const [cellSize, setCellSize] = useState(TARGET_CELL_SIZE); + const [dims, setDims] = useState({ cols: 0, rows: 0 }); + const [rotations, setRotations] = useState([]); + const currentCellIdx = useRef(-1); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const obs = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + const cols = Math.max(1, Math.round(width / TARGET_CELL_SIZE)); + const size = width / cols; + const rows = size > 0 ? Math.ceil(height / size) : 0; + const count = cols * rows; + setCellSize(size); + setDims({ cols, rows }); + setRotations( + Array.from( + { length: count }, + () => ROTATIONS[Math.floor(Math.random() * 4)], + ), + ); + currentCellIdx.current = -1; + }); + obs.observe(el); + return () => obs.disconnect(); + }, []); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + const el = containerRef.current; + if (!el || dims.cols === 0 || cellSize === 0) return; + const rect = el.getBoundingClientRect(); + const cellX = Math.floor((e.clientX - rect.left) / cellSize); + const cellY = Math.floor((e.clientY - rect.top) / cellSize); + if (cellX < 0 || cellY < 0 || cellX >= dims.cols || cellY >= dims.rows) + return; + const idx = cellY * dims.cols + cellX; + if (idx === currentCellIdx.current) return; + currentCellIdx.current = idx; + setRotations((prev) => { + const next = [...prev]; + next[idx] = ((next[idx] ?? 0) + 90) % 360; + return next; + }); + }, + [dims, cellSize], + ); + + const handleMouseLeave = useCallback(() => { + currentCellIdx.current = -1; + }, []); + + return ( +
+ {rotations.map((rotation, i) => ( +
+
+ +
+
+ ))} +
+ ); +} diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx new file mode 100644 index 0000000000..db975bf6fc --- /dev/null +++ b/apps/web/src/components/sidebar.tsx @@ -0,0 +1,635 @@ +import { Link, useRouterState } from "@tanstack/react-router"; +import { + BookOpen, + Building2, + ChevronRight, + FileText, + History, + LayoutTemplate, + Map, + Menu, + MessageCircle, + Newspaper, + X, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { cn } from "@hypr/utils"; + +import { SearchTrigger } from "@/components/search"; +import { getPlatformCTA, usePlatform } from "@/hooks/use-platform"; + +const featuresList = [ + { to: "/product/ai-notetaking", label: "AI Notetaking" }, + { to: "/product/search", label: "Searchable Notes" }, + { to: "/gallery?type=template", label: "Custom Templates" }, + { to: "/product/markdown", label: "Markdown Files" }, + { to: "/product/flexible-ai", label: "Flexible AI" }, + { to: "/opensource", label: "Open Source" }, +]; + +const solutionsList = [ + { to: "/solution/knowledge-workers", label: "For Knowledge Workers" }, + { to: "/enterprise", label: "For Enterprises" }, + { to: "/product/api", label: "For Developers" }, +]; + +const resourcesList: { + to: string; + label: string; + icon: LucideIcon; + external?: boolean; +}[] = [ + { to: "/blog/", label: "Blog", icon: FileText }, + { to: "/docs/", label: "Documentation", icon: BookOpen }, + { + to: "/gallery?type=template", + label: "Meeting Templates", + icon: LayoutTemplate, + }, + { to: "/updates/", label: "Updates", icon: Newspaper }, + { to: "/changelog/", label: "Changelog", icon: History }, + { to: "/roadmap/", label: "Roadmap", icon: Map }, + { to: "/company-handbook/", label: "Company Handbook", icon: Building2 }, + { + to: "https://discord.gg/hyprnote", + label: "Community", + icon: MessageCircle, + external: true, + }, +]; + +const homeSections = [ + { id: "hero", label: "Intro" }, + { id: "how-it-works", label: "How it works" }, + { id: "ai", label: "AI features" }, + { id: "grows-with-you", label: "Grows with you" }, + { id: "solutions", label: "Solutions" }, + { id: "faq", label: "FAQ" }, + { id: "blog", label: "Blog" }, +]; + +const homeSectionIds = homeSections.map((s) => s.id); + +function useActiveHomeSection(enabled: boolean) { + const [activeId, setActiveId] = useState(null); + + useEffect(() => { + if (!enabled) { + setActiveId(null); + return; + } + + setActiveId(homeSectionIds[0]); + + const observer = new IntersectionObserver( + (entries) => { + const visible = entries.find((e) => e.isIntersecting); + if (visible) { + setActiveId(visible.target.id); + } + }, + { rootMargin: "-20% 0px -60% 0px", threshold: 0 }, + ); + + homeSectionIds.forEach((id) => { + const el = document.getElementById(id); + if (el) observer.observe(el); + }); + + return () => observer.disconnect(); + }, [enabled]); + + return activeId; +} + +function findActiveSubItem(pathname: string) { + const candidates = [ + ...featuresList.map((i) => ({ ...i, parent: "Product" })), + ...solutionsList.map((i) => ({ ...i, parent: "Product" })), + ...resourcesList + .filter((i) => !i.external) + .map((i) => ({ ...i, parent: "Resources" })), + ]; + return ( + candidates.find((item) => + pathname.startsWith(item.to.replace(/\/$/, "")), + ) ?? null + ); +} + +function CharLogo({ className }: { className?: string }) { + return ( + + + + + + + + + ); +} + +const navLinks = [ + { to: "/why-char/", label: "Why Char" }, + { to: "/product/ai-notetaking/", label: "Product", hasSubmenu: true }, + { to: "/docs/", label: "Resources", hasSubmenu: true }, + { to: "/pricing/", label: "Pricing" }, +] as const; + +export function Sidebar() { + const [isMobileOpen, setIsMobileOpen] = useState(false); + const router = useRouterState(); + const platform = usePlatform(); + const platformCTA = getPlatformCTA(platform); + const pathname = router.location.pathname; + const isHomePage = pathname === "/"; + const activeSection = useActiveHomeSection(isHomePage); + const activeSubItem = isHomePage ? null : findActiveSubItem(pathname); + + useEffect(() => { + setIsMobileOpen(false); + }, [pathname]); + + useEffect(() => { + if (isMobileOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [isMobileOpen]); + + return ( + <> + + + {isMobileOpen && ( +
setIsMobileOpen(false)} + /> + )} + + + + {/* Mobile slide-out sidebar */} + + + ); +} + +function HomeSectionNav({ activeId }: { activeId: string | null }) { + const scrollTo = (id: string) => { + document.getElementById(id)?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }; + + return ( + + ); +} + +function MobileTopBar({ + isMobileOpen, + setIsMobileOpen, +}: { + isMobileOpen: boolean; + setIsMobileOpen: (open: boolean) => void; +}) { + return ( +
+ + + + +
+ ); +} + +function SidebarFlyout({ + label, + to, + isActive, + activeSubItem, +}: { + label: string; + to: string; + isActive: boolean; + activeSubItem: { to: string; label: string } | null; +}) { + const [isOpen, setIsOpen] = useState(false); + const timeoutRef = useRef | null>(null); + + const open = () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setIsOpen(true); + }; + + const close = () => { + timeoutRef.current = setTimeout(() => setIsOpen(false), 150); + }; + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + return ( +
+ + {label} + + + + {activeSubItem && ( + + {activeSubItem.label} + + )} + + {isOpen && ( +
+
+ {label === "Product" && } + {label === "Resources" && } +
+
+ )} +
+ ); +} + +function ProductFlyoutContent() { + return ( +
+
+ + Features + +
+ {featuresList.map((item) => ( + + {item.label} + + ))} +
+
+ + Solutions + +
+ {solutionsList.map((item) => ( + + {item.label} + + ))} +
+ ); +} + +function ResourcesFlyoutContent() { + return ( +
+ {resourcesList.map((item) => { + const Icon = item.icon; + if (item.external) { + return ( + + + {item.label} + + ); + } + return ( + + + {item.label} + + ); + })} +
+ ); +} + +function MobileSubmenu({ + label, + isActive, +}: { + label: string; + to: string; + isActive: boolean; +}) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+ + + {isExpanded && ( +
+ {label === "Product" && ( + <> + + Features + + {featuresList.map((item) => ( + + {item.label} + + ))} + + Solutions + + {solutionsList.map((item) => ( + + {item.label} + + ))} + + )} + {label === "Resources" && ( + <> + {resourcesList.map((item) => { + const IconComp = item.icon; + if (item.external) { + return ( + + + {item.label} + + ); + } + return ( + + + {item.label} + + ); + })} + + )} +
+ )} +
+ ); +} + +function SidebarCTA({ + platformCTA, +}: { + platformCTA: ReturnType; +}) { + const baseClass = + "flex h-9 items-center justify-center rounded-lg bg-neutral-800 text-sm text-neutral-300 transition-colors hover:bg-neutral-700 hover:text-neutral-100"; + + if (platformCTA.action === "download") { + return ( + + {platformCTA.label} + + ); + } + + return ( + + {platformCTA.label} + + ); +} diff --git a/apps/web/src/routes/_view/about.tsx b/apps/web/src/routes/_view/about.tsx index d486b36a67..b92cfe2dd9 100644 --- a/apps/web/src/routes/_view/about.tsx +++ b/apps/web/src/routes/_view/about.tsx @@ -97,11 +97,8 @@ function Component() { }; return ( -
-
+
+
-
-

+
+

About

@@ -215,7 +212,7 @@ function OurStoryGrid({

+
+ {mutation.isSuccess && ( +

+ Thanks! We'll be in touch soon. +

+ )} + {mutation.isError && ( +

+ {mutation.error instanceof Error + ? mutation.error.message + : "Something went wrong. Please try again."} +

+ )} + {!mutation.isSuccess && !mutation.isError && - !mutation.isSuccess && - "border-neutral-200 focus-within:border-stone-500", - ])} - > - field.handleChange(e.target.value)} - onBlur={field.handleBlur} - placeholder={heroCTA.inputPlaceholder} - className="flex-1 bg-white px-6 py-4 text-base outline-hidden" - disabled={mutation.isPending || mutation.isSuccess} - /> - -
- {mutation.isSuccess && ( -

- Thanks! We'll be in touch soon. -

- )} - {mutation.isError && ( -

- {mutation.error instanceof Error - ? mutation.error.message - : "Something went wrong. Please try again."} -

+ (heroCTA.subtextLink ? ( + + {heroCTA.subtext} + + ) : ( +

+ {heroCTA.subtext} +

+ ))} + )} - {!mutation.isSuccess && - !mutation.isError && - (heroCTA.subtextLink ? ( - - {heroCTA.subtext} - - ) : ( -

- {heroCTA.subtext} -

- ))} - - )} - - - ) : ( -
- - {heroCTA.subtextLink ? ( - - {heroCTA.subtext} - + + ) : ( -

{heroCTA.subtext}

+
+ + {heroCTA.subtextLink ? ( + + {heroCTA.subtext} + + ) : ( +

+ {heroCTA.subtext} +

+ )} +
)}
- )} +
+ +
+ +
+
+

-
+ {/*
onVideoExpand(MUX_PLAYBACK_ID)} @@ -390,229 +374,28 @@ function HeroSection({ onPlay={() => onVideoExpand(MUX_PLAYBACK_ID)} />
-
+
*/}
); } -function ValuePropsGrid({ - valueProps, -}: { - valueProps: ReadonlyArray<{ - readonly title: string; - readonly description: string; - }>; -}) { +function LogoSection() { return ( -
- {valueProps.map((prop, index) => ( -
-

- {prop.title} -

-

- {prop.description} -

-
- ))} -
- ); -} - -function TestimonialsSection() { - return ( -
-
-

- Loved by professionals at -

- - - -
- - -
-
+
+

+ Loved by professionals in: +

+
); } -function TestimonialsMobileGrid() { - return ( -
- - - - - - - -
- ); -} - -function TestimonialsDesktopGrid() { - return ( -
-
- -
- -
- -
- -
- -
- -
- -
-
- ); -} - export function CoolStuffSection() { return (
-
-

+

+

Secure by Design

@@ -625,7 +408,7 @@ export function CoolStuffSection() { icon="mdi:robot-off-outline" className="text-3xl text-stone-600" /> -

No bots

+

No bots

Captures system audio—no bots join your calls. @@ -643,7 +426,7 @@ export function CoolStuffSection() {

-

+

Fully local option

@@ -669,7 +452,7 @@ export function CoolStuffSection() { icon="mdi:robot-off-outline" className="text-2xl text-stone-600" /> -

No bots

+

No bots

Captures system audio—no bots join your calls. @@ -687,7 +470,7 @@ export function CoolStuffSection() {

-

+

Fully local option

@@ -709,119 +492,180 @@ export function CoolStuffSection() { } export function HowItWorksSection() { - const [typedText1, setTypedText1] = useState(""); - const [typedText2, setTypedText2] = useState(""); const [enhancedLines, setEnhancedLines] = useState(0); - const text1 = "metrisc w/ john"; - const text2 = "stakehlder mtg"; - useEffect(() => { const runAnimation = () => { - setTypedText1(""); - setTypedText2(""); setEnhancedLines(0); - 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); - + setEnhancedLines(1); + setTimeout(() => { + setEnhancedLines(2); + setTimeout(() => { + setEnhancedLines(3); + setTimeout(() => { + setEnhancedLines(4); + setTimeout(() => { + setEnhancedLines(5); setTimeout(() => { - setEnhancedLines(1); + setEnhancedLines(6); setTimeout(() => { - setEnhancedLines(2); - setTimeout(() => { - setEnhancedLines(3); - setTimeout(() => { - setEnhancedLines(4); - setTimeout(() => { - setEnhancedLines(5); - setTimeout(() => { - setEnhancedLines(6); - setTimeout(() => { - setEnhancedLines(7); - setTimeout(() => runAnimation(), 1000); - }, 800); - }, 800); - }, 800); - }, 800); - }, 800); + setEnhancedLines(7); + setTimeout(() => runAnimation(), 1000); }, 800); - }, 500); - } - }, 50); - } - }, 50); - }, 500); + }, 800); + }, 800); + }, 800); + }, 800); + }, 800); + }, 800); }; runAnimation(); }, []); return ( -
-
-

- How it works -

-
-
-
-
-

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

+
+ {/* Header */} +
+

+ Focus on conversation while Char makes notes for you +

+
+ + {/* Block 1: Listen & Write */} +
+
+
+ +
+

+ Char listens and keeps track of everything that happens during the meeting.

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

Meeting in progress...

+
+ +
+
+
+ {/* Notes panel */} +
+
+
+
+
+
+
+
+ + my notes.md + +
-
- {typedText2} - {typedText2 && typedText2.length < text2.length && ( - | - )} + +
+
+ {"ui update - moble\napi\nnew dash - urgnet"} +
- +
+ {[0, 1, 2].map((i) => ( +
+
+
+ ))} +
+
+ {" "} + {" "} +
+
+ {" "} + {" "} +
+
+ {" "} + {" "} +
+
+ {" "} + {" "} +
+
+
+
-
-
-

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

+
+
+ + + +
+
+ + {/* Block 2: Summarize */} +
+
+
+ +
+

+ After the meeting is over, Char combines your notes with + transcripts to create a perfect 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 +739,513 @@ export function HowItWorksSection() {

- +
+
+
+ + {/* features block */} +
+ {/* local or cloud */} +
+
+ +
+ +
+
+
+

+ Local or cloud, your choice +

+

+ Use local models or bring your own API key. Works without + internet. +

+
+ + {/* upload existing recordings */} +
+
+ {["Claude cowork", "Openclaw", "Codex", "Claude Code"].map( + (wf) => ( +
+ {wf} +
+ ), + )} +
+
+

+ Create
any workflow +

+

+ Char is fully avaliable to any agent because of it's + markdown-first nature +

+
+
+ + {/* no bot on calls */} +
+
+
+
+ +
+

1-1 with Joanna

+

+ AI Notetaker joined. +

+
+
+ +
+
+
+

+ No bot on calls +

+

+ Char captures system audio directly. No faceless bots join your + meetings. +

+
+
+ + {/* feature 4 */} +
+
+
+
+ +
+
+ + + + + + + + + +
+

+ Meeting.12.03.26.wav +

+

14:30:25

+
+
+
+
+
+

+ Upload existing recordings +

+

+ Drop in audio files or transcripts to turn them into searchable + notes. +

+
+
+
+
+
+ ); +} + +function ChatBubbleQuestion({ text }: { text: string }) { + return ( +
+
+

{text}

+
+
+ ); +} + +function ChatBubbleResponse({ + text, + withCheck, +}: { + text: string; + withCheck?: boolean; +}) { + return ( +
+

Char

+ {withCheck ? ( +
+ + {text} +
+ ) : ( +

{text}

+ )} +
+ ); +} + +function ChatInput() { + return ( +
+
+ + Ask Char anything... + +
+ +
+
+
+ ); +} + +function WorkflowGraphic() { + const [step, setStep] = useState(0); + + useEffect(() => { + const t1 = setTimeout(() => setStep(1), 200); + const t2 = setTimeout(() => setStep(2), 800); + const t3 = setTimeout(() => setStep(3), 3200); + return () => { + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + }; + }, []); + + return ( +
+
+ + {step >= 1 && ( + + + + )} + {step >= 2 && ( + + + + )} + {step >= 3 && ( + + + + )} + +
+ +
+ ); +} + +function LiveGraphic() { + const [loopKey, setLoopKey] = useState(0); + const [step, setStep] = useState(0); + + useEffect(() => { + setStep(0); + const t1 = setTimeout(() => setStep(1), 200); + const t2 = setTimeout(() => setStep(2), 800); + const t3 = setTimeout(() => setStep(3), 2800); + return () => { + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + }; + }, [loopKey]); + + useEffect(() => { + const id = setInterval(() => setLoopKey((k) => k + 1), 6500); + return () => clearInterval(id); + }, []); + + return ( +
+
+
+
+ + Weekly Team Sync + + 42:17
+
-
-
-
-

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

+ + {step >= 1 && ( + + + + )} + {step >= 2 && ( + + + + )} + {step >= 3 && ( + + + + )} + +
+ + +
+ ); +} + +export function AISection() { + return ( +
+
+

+ Get more from every note with AI +

+

+ Ask questions, execute tasks, and grow your knowledge base—all from + your meeting notes. +

+
+ +
+ {/* Block 1: Search */} +
+
+ +
+
+

+ Ask anything about your meetings +

+

+ Query your entire conversation history. Find decisions, action + items, or topics discussed in previous meetings in natural + language.

-
- -
-
ui update - moble
-
api
-
new dash - urgnet
-
a/b tst next wk
-
- {typedText1} - {typedText1 && typedText1.length < text1.length && ( - | - )} -
-
- {typedText2} - {typedText2 && typedText2.length < text2.length && ( - | - )} -
-
-
+
+ + {/* Block 2: Workflow */} +
+
+ +
+
+

+ Execute workflows and tasks +

+

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

+
+ + + +
-
-
-

- After the meeting is over,{" "} - Char combines your notes with transcripts to create a perfect - summary. + {/* Block 3: Live */} +

+
+ +
+
+

+ Chat during live meetings +

+

+ Get instant answers from the current transcript and past meeting + context without breaking your flow.

-
- -
-
-

- 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", - ])} - > - The UI update will be implemented in phases, starting with - core navigation improvements. Ben will ensure API - modifications are completed before development begins. -
  • -
-
-
-

- New Dashboard – Urgent Priority -

-
    -
  • = 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. -
  • -
-
-
-
+
+
+
+ ); +} + +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 + + +
+ +
+
+
+

+ Have 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 + +
  • +
+
+
+ +
+
+

+ 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 + +
  • +
+
@@ -1116,7 +1324,7 @@ export function MainFeaturesSection({ return (
-
+
-

+

Works like charm

@@ -1222,7 +1430,7 @@ function FeaturesMobileCarousel({ icon={feature.icon} className="text-2xl text-stone-600" /> -

+

{feature.title}

@@ -1429,7 +1637,7 @@ function FeaturesDesktopGrid() {
-

+

{feature.title}

@@ -1473,8 +1681,8 @@ const templateCategories = [ export function TemplatesSection() { return (
-
-

+
+

A template for every meeting

@@ -1486,7 +1694,7 @@ export function TemplatesSection() { -

+
-

+

{category.category}

@@ -1556,7 +1764,7 @@ function TemplatesDesktopView() { >
-

+

{category.category}

@@ -1580,12 +1788,235 @@ function TemplatesDesktopView() { ); } +const solutionColors: Record< + string, + { accent: string; bg: string; border: string } +> = { + sales: { accent: "#b45309", bg: "#fefce8", border: "#fde68a" }, + research: { accent: "#1d4ed8", bg: "#eff6ff", border: "#bfdbfe" }, + legal: { accent: "#374151", bg: "#f9fafb", border: "#d1d5db" }, + engineering: { accent: "#6d28d9", bg: "#f5f3ff", border: "#ddd6fe" }, + healthcare: { accent: "#047857", bg: "#ecfdf5", border: "#a7f3d0" }, + recruiting: { accent: "#be185d", bg: "#fff1f2", border: "#fecdd3" }, + "project-management": { accent: "#c2410c", bg: "#fff7ed", border: "#fed7aa" }, + journalism: { accent: "#0f766e", bg: "#f0fdfa", border: "#99f6e4" }, +}; + +const solutionScenarios = [ + { + id: "sales", + label: "Sales", + icon: "mdi:briefcase-outline", + headline: "Close more deals, capture every detail", + description: + "Stop context-switching between notes and conversations. Char records every sales call, extracts deal insights, and prepares follow-ups automatically.", + bullets: [ + "Capture buying signals, objections, and commitments", + "Auto-generate action items after every call", + "Keep sensitive deal data on your device", + ], + link: "/solution/sales/", + }, + { + id: "research", + label: "Research", + icon: "mdi:flask-outline", + headline: "Turn every interview into structured insight", + description: + "Record user interviews and field sessions without missing a word. AI identifies themes, extracts quotes, and surfaces patterns across your entire research corpus.", + bullets: [ + "Accurate transcripts for qualitative analysis", + "Theme and pattern identification across sessions", + "Participant data stays private on your machine", + ], + link: "/solution/research", + }, + { + id: "legal", + label: "Legal", + icon: "mdi:scale-balance", + headline: "Privilege-protected notes, nothing leaves your device", + description: + "Attorney-client confidentiality demands local AI. Char captures client consultations, depositions, and strategy sessions with complete data sovereignty.", + bullets: [ + "Local AI — no cloud processing of privileged content", + "Searchable archive of all case-related meetings", + "Accurate billing documentation from meeting records", + ], + link: "/solution/legal", + }, + { + id: "engineering", + label: "Engineering", + icon: "mdi:code-braces", + headline: "The meeting tool you can actually fork", + description: + "Bring your own keys, build React extensions, run local models. Char is open source, self-hostable, and designed to fit into your existing dev workflow.", + bullets: [ + "Shell hooks and API for custom automation", + "Self-host on your own infrastructure", + "Full open-source codebase you can inspect and modify", + ], + link: "/solution/engineering", + }, + { + id: "healthcare", + label: "Healthcare", + icon: "mdi:hospital-building", + headline: "Clinical notes without the administrative burden", + description: + "Focus on your patient, not your keyboard. Char captures consultations with local processing that keeps health data off third-party servers.", + bullets: [ + "Local AI keeps patient data on your device", + "Structured notes from every consultation", + "Searchable records across all patient interactions", + ], + link: "/solution/healthcare", + }, + { + id: "recruiting", + label: "Recruiting", + icon: "mdi:account-search-outline", + headline: "Remember every candidate, make better hires", + description: + "Capture every interview with full transcripts and AI summaries. Compare candidates with objective meeting records instead of fading memory.", + bullets: [ + "Full transcripts for every interview", + "AI-generated candidate summaries", + "Share structured notes across the hiring team", + ], + link: "/solution/recruiting", + }, + { + id: "project-management", + label: "Project Mgmt", + icon: "mdi:clipboard-check-outline", + headline: "Turn standup chaos into clear action", + description: + "Every decision, blocker, and commitment captured automatically. Char turns planning sessions into structured notes with owners and deadlines.", + bullets: [ + "Auto-extracted action items and owners", + "Decision log for every project meeting", + "Searchable context for async teammates", + ], + link: "/solution/project-management", + }, + { + id: "journalism", + label: "Journalism", + icon: "mdi:newspaper-variant-outline", + headline: "Every source, every quote, perfectly on record", + description: + "Record interviews and press briefings with accurate transcripts. Never misquote a source or lose context when writing under deadline.", + bullets: [ + "Verbatim transcripts ready for fact-checking", + "Search quotes across all interviews instantly", + "Local storage keeps source conversations confidential", + ], + link: "/solution/journalism", + }, +]; + +function SolutionsTabbar() { + const [activeId, setActiveId] = useState(solutionScenarios[0].id); + const active = + solutionScenarios.find((s) => s.id === activeId) ?? solutionScenarios[0]; + const activeColor = solutionColors[active.id]; + + return ( +
+
+

+ Build for every conversation +

+
+ + {/* Folder tabs */} +
+ {solutionScenarios.map((scenario) => { + const isActive = scenario.id === activeId; + const c = solutionColors[scenario.id]; + return ( + + ); + })} +
+ + {/* Content block */} + + +
+

+ {active.headline} +

+

+ {active.description} +

+ + Learn more + + +
+ +
    + {active.bullets.map((bullet) => ( +
  • + + + {bullet} + +
  • + ))} +
+
+
+
+ ); +} + function FAQSection() { return ( -
-
-
-

+
+
+
+

Frequently Asked Questions

@@ -1625,90 +2056,6 @@ function FAQSection() { ); } -function ManifestoSection() { - return ( -
-
-
-
-

- Our manifesto -

- -
-

- We believe in the power of notetaking, not notetakers. Meetings - should be moments of presence, not passive attendance. If you - are not adding value, your time is better spent elsewhere for - you and your team. -

-

- Char 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 together. -

-

- We stand with those who value real connection and purposeful - collaboration. -

-
- -
- John Jeong - Yujong Lee -
- -
-
-

- Char -

-

- John Jeong, Yujong Lee -

-
- -
- Char Signature -
-
-
-
-
-
- ); -} - function BlogSection() { const sortedArticles = [...allArticles] .sort((a, b) => { @@ -1723,12 +2070,12 @@ function BlogSection() { } return ( -
-
-

+
+
+

Latest from our blog

-

+

Insights, updates, and stories from the Char team

@@ -1756,7 +2103,7 @@ function BlogSection() {

-

+

{article.display_title || article.meta_title}

@@ -1787,7 +2134,7 @@ function BlogSection() { })}
-
+
-
+
-

+

Your meetings. Your data.
Your control.

diff --git a/apps/web/src/routes/_view/integrations/$category.$slug.tsx b/apps/web/src/routes/_view/integrations/$category.$slug.tsx index 0d2d3f11bb..7733cc7c4e 100644 --- a/apps/web/src/routes/_view/integrations/$category.$slug.tsx +++ b/apps/web/src/routes/_view/integrations/$category.$slug.tsx @@ -70,11 +70,8 @@ function Component() { }; return ( -
-
+
+
-
+
-

+

{headline}

diff --git a/apps/web/src/routes/_view/jobs/$slug.tsx b/apps/web/src/routes/_view/jobs/$slug.tsx index 674adaaceb..f8807df1cb 100644 --- a/apps/web/src/routes/_view/jobs/$slug.tsx +++ b/apps/web/src/routes/_view/jobs/$slug.tsx @@ -64,11 +64,8 @@ function JobPage() { const { job } = Route.useLoaderData(); return ( -

-
+
+
@@ -107,10 +104,10 @@ function HeroSection({ job }: { job: (typeof allJobs)[0] }) { />
-
+

full-time, remote @@ -136,7 +133,7 @@ function JobDetailsSection({ job }: { job: (typeof allJobs)[0] }) { components={{ a: MDXLink, h2: ({ children }) => ( -

+

{stripLinks(children)}

), @@ -183,7 +180,7 @@ function stripLinks(children: ReactNode): ReactNode { function CTASection({ job }: { job: (typeof allJobs)[0] }) { return (
-
+
-

Interested?

+

Interested?

We'd love to hear from you.

diff --git a/apps/web/src/routes/_view/jobs/index.tsx b/apps/web/src/routes/_view/jobs/index.tsx index 85cdcf1b12..17f970a520 100644 --- a/apps/web/src/routes/_view/jobs/index.tsx +++ b/apps/web/src/routes/_view/jobs/index.tsx @@ -27,11 +27,8 @@ export const Route = createFileRoute("/_view/jobs/")({ function JobsPage() { return ( -
-
+
+
@@ -43,8 +40,8 @@ function JobsPage() { function HeroSection() { return (
-
-

+
+

Jobs

@@ -70,7 +67,7 @@ function JobsSection() { ))}

) : ( -
+

There are no open positions at the moment.

@@ -128,7 +125,7 @@ function JobCard({ function CTASection() { return (
-
+
-

+

Don't see a role that fits?

diff --git a/apps/web/src/routes/_view/legal/$slug.tsx b/apps/web/src/routes/_view/legal/$slug.tsx index 2449ce3be7..c52f86447d 100644 --- a/apps/web/src/routes/_view/legal/$slug.tsx +++ b/apps/web/src/routes/_view/legal/$slug.tsx @@ -42,11 +42,8 @@ function Component() { const { doc } = Route.useLoaderData(); return ( -

-
+
+

{doc.title}

diff --git a/apps/web/src/routes/_view/legal/index.tsx b/apps/web/src/routes/_view/legal/index.tsx index d64aa4ea7e..d82fc9e973 100644 --- a/apps/web/src/routes/_view/legal/index.tsx +++ b/apps/web/src/routes/_view/legal/index.tsx @@ -30,13 +30,10 @@ export const Route = createFileRoute("/_view/legal/")({ function Component() { return ( -
-
+
+
-

+

Legal

@@ -68,7 +65,7 @@ function LegalCard({ doc }: { doc: (typeof allLegals)[number] }) { className="mt-0.5 shrink-0 text-xl text-stone-700 transition-colors group-hover:text-stone-800" />

-

+

{doc.title}

diff --git a/apps/web/src/routes/_view/opensource.tsx b/apps/web/src/routes/_view/opensource.tsx index 154586b5d0..dc411a92a9 100644 --- a/apps/web/src/routes/_view/opensource.tsx +++ b/apps/web/src/routes/_view/opensource.tsx @@ -58,11 +58,8 @@ function Component() { const heroInputRef = useRef(null); return ( -

-
+
+
@@ -141,8 +138,8 @@ function HeroSection() { {stargazers.length > 0 && }
-
-

+
+

Built in the open,
for everyone @@ -178,14 +175,14 @@ function LetterSection() { return (
-
+
A letter from our team
-

+

Why Open Source is Inevitable
in the Age of AI @@ -425,10 +422,10 @@ function TechStackSection() {
-

+

Our Tech Stack

-

+

Built with modern, privacy-respecting technologies that run locally on your device.

@@ -439,7 +436,7 @@ function TechStackSection() { return (
-

+

{section.category}

@@ -518,10 +515,10 @@ function SponsorsSection() {
-

+

Paying It Forward

-

+

We love giving back to the community that makes Char possible. As we grow, we hope to sponsor even more projects and creators.

@@ -529,7 +526,7 @@ function SponsorsSection() {
-

+

Projects We Sponsor

@@ -576,7 +573,7 @@ function SponsorsSection() { })}
-

+

We Appreciate Your Support

@@ -689,7 +686,7 @@ function StatCard({ return (

setIsHovered(true)} @@ -761,10 +758,10 @@ function ProgressSection() {
-

+

How We're Doing

-

+

Our progress is measured by the community we're building together.

@@ -837,10 +834,10 @@ function JoinMovementSection() {
-

+

Be Part of the Movement

-

+

Every contribution, no matter how small, helps build a more private future for AI.

diff --git a/apps/web/src/routes/_view/oss-friends.tsx b/apps/web/src/routes/_view/oss-friends.tsx index b1670eb659..fb69c9579d 100644 --- a/apps/web/src/routes/_view/oss-friends.tsx +++ b/apps/web/src/routes/_view/oss-friends.tsx @@ -68,11 +68,8 @@ function Component() { }; return ( -
-
+
+
-
-

+
+

OSS Friends

@@ -215,8 +212,8 @@ function FriendsSection({ function JoinSection() { return (

-
-

+
+

Want to be listed?

diff --git a/apps/web/src/routes/_view/press-kit/app.tsx b/apps/web/src/routes/_view/press-kit/app.tsx index 130a7688c6..3ebd29066c 100644 --- a/apps/web/src/routes/_view/press-kit/app.tsx +++ b/apps/web/src/routes/_view/press-kit/app.tsx @@ -126,11 +126,8 @@ function Component() { }; return ( -

-
+
+
-
-

+
+

App Screenshots

@@ -249,7 +246,7 @@ function ScreenshotsGrid({ data: screenshot, }) } - className="group flex h-fit cursor-pointer flex-col items-center rounded-lg p-4 text-center transition-colors hover:bg-stone-50" + className="group flex h-fit cursor-pointer flex-col items-center rounded-lg p-4 text-left transition-colors hover:bg-stone-50" >

-
+
+
-
-

+
+

Press Kit

@@ -155,7 +152,7 @@ function FinderFolder({ -

+
+
+ @@ -111,11 +109,31 @@ function Component() { ); } +function TeamPricingBanner() { + return ( +
+ + Early Bird Discount: Get 68% off as we launch our new + version and help with migration —{" "} + offer extended while we finalize the new timeline + +
+ ); +} + + function HeroSection() { return ( -
+
-

+

Pricing

@@ -149,14 +167,14 @@ function PricingCard({ plan }: { plan: PricingPlan }) { ])} > {plan.popular && ( -

+
Most Popular
)}
-

+

{plan.name}

{plan.description}

@@ -164,7 +182,7 @@ function PricingCard({ plan }: { plan: PricingPlan }) { {plan.price ? (
- + ${plan.price.monthly} /month @@ -174,7 +192,7 @@ function PricingCard({ plan }: { plan: PricingPlan }) {
) : ( -
Free
+
Free
)}
@@ -313,7 +331,7 @@ function FAQSection() { return (
-

+

Frequently Asked Questions

@@ -337,7 +355,7 @@ function FAQSection() { function CTASection() { return (
-
+
-

Need a team plan?

+

Need a team plan?

Book a call to discuss custom team pricing and enterprise solutions

diff --git a/apps/web/src/routes/_view/privacy.tsx b/apps/web/src/routes/_view/privacy.tsx index ad47a453ec..224366f024 100644 --- a/apps/web/src/routes/_view/privacy.tsx +++ b/apps/web/src/routes/_view/privacy.tsx @@ -36,11 +36,8 @@ export const Route = createFileRoute("/_view/privacy")({ function Component() { return ( -
-
+
+
@@ -63,12 +60,12 @@ function HeroSection() { return (
-
+
Privacy-first by design
-

+

Your conversations
belong to you @@ -115,10 +112,10 @@ function PrivacyPromiseSection() { return (
-

+

Our privacy promise

-

+

These aren't just policies—they're principles embedded in our architecture. We couldn't violate your privacy even if we wanted to.

@@ -132,7 +129,7 @@ function PrivacyPromiseSection() { icon={promise.icon} className="mb-4 text-3xl text-stone-600" /> -

+

{promise.title}

{promise.description}

@@ -147,12 +144,12 @@ function DataOwnershipSection() { return (
-
+
-

+

You own your data, completely

@@ -162,7 +159,7 @@ function DataOwnershipSection() {

-
+
-
+
-
+
-

+

Optional sync, your choice

@@ -226,12 +223,12 @@ function NoTrackingSection() { return (

-
+
-

+

No tracking, no profiling

@@ -307,12 +304,12 @@ function TransparencySection() { return (

-
+
-

+

Verify, don't trust

@@ -327,7 +324,7 @@ function TransparencySection() { icon="mdi:source-repository" className="mb-4 text-3xl text-stone-600" /> -

+

Open source code

@@ -341,7 +338,7 @@ function TransparencySection() { icon="mdi:file-document-check" className="mb-4 text-3xl text-stone-600" /> -

+

Clear documentation

@@ -414,8 +411,8 @@ function PrivacyComparisonSection() { return (

-
-

+
+

How we compare

@@ -431,10 +428,10 @@ function PrivacyComparisonSection() { Feature - + Char - + Others @@ -443,7 +440,7 @@ function PrivacyComparisonSection() { {comparisons.map((row, index) => ( {row.feature} - + - + {row.others} @@ -468,12 +465,12 @@ function PrivacyComparisonSection() { function CTASection() { return (

-
+
-

+

Take back control of your meeting data

diff --git a/apps/web/src/routes/_view/product/ai-assistant.tsx b/apps/web/src/routes/_view/product/ai-assistant.tsx index a66d80b31b..e64e861ea8 100644 --- a/apps/web/src/routes/_view/product/ai-assistant.tsx +++ b/apps/web/src/routes/_view/product/ai-assistant.tsx @@ -1,9 +1,23 @@ 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 { + JiraToolCall, + SearchToolCall, + TranscriptToolCall, +} from "@/components/ai-feature-panel"; +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,20 +35,52 @@ 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,204 +90,621 @@ 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?" - -
  • -
  • - - - "Show me all action items from previous meetings" - -
  • -
+
+ +
+ ))} +
+ + ); +} -
- -

- 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. +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; + +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 bg-transparent text-sm text-stone-700 outline-none placeholder:text-neutral-400" + /> +
+ +
+
+
+ )} +
+ )} +
+ +
+ ); +} -
- -

- 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 -

+

Suggestions

@@ -253,7 +716,7 @@ function DuringMeetingSection() {

Talk time tracking @@ -267,7 +730,7 @@ function DuringMeetingSection() {
ELI5 explanations @@ -278,16 +741,6 @@ function DuringMeetingSection() {

- -
- - Learn more about extensions - - -
@@ -295,243 +748,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. +

    @@ -542,7 +1125,7 @@ function AfterMeetingSection() { function CTASection() { return (
    -
    +
    -

    +

    Start using your AI assistant

    @@ -588,7 +1171,7 @@ function CTASection() { to="/product/ai-notetaking/" className={cn([ "flex h-12 items-center justify-center px-6 text-base sm:text-lg", - "rounded-full border border-neutral-300 text-stone-700", + "rounded-full border border-neutral-300 text-stone-600", "transition-colors hover:bg-white", ])} > diff --git a/apps/web/src/routes/_view/product/ai-notetaking.tsx b/apps/web/src/routes/_view/product/ai-notetaking.tsx index cf199bdd8a..5a0aeb08e1 100644 --- a/apps/web/src/routes/_view/product/ai-notetaking.tsx +++ b/apps/web/src/routes/_view/product/ai-notetaking.tsx @@ -72,11 +72,8 @@ const tabs = [ function Component() { return ( -

    -
    +
    +
    @@ -101,8 +98,8 @@ function HeroSection() { return (
    -
    -

    +
    +

    AI Notepad for Smarter Meeting Notes

    @@ -140,7 +137,7 @@ function EditorSection() {

    -

    +

    Simple, Familiar Notepad

    @@ -180,7 +177,7 @@ function EditorSection() {

    -

    +

    Simple, Familiar Notepad

    @@ -597,8 +594,8 @@ function AnimatedMarkdownDemo({ isMobile = false }: { isMobile?: boolean }) { function TranscriptionSection() { return (

    -
    -

    +
    +

    Live meetings to recorded audio, Char transcribes it all

    @@ -612,7 +609,7 @@ function TranscriptionSection() { icon="mdi:microphone" className="text-3xl text-stone-700" /> -

    +

    Real-time transcription

    @@ -633,7 +630,7 @@ function TranscriptionSection() {
    -

    +

    Upload files

    @@ -656,7 +653,7 @@ function TranscriptionSection() { icon="mdi:microphone" className="text-2xl text-stone-700" /> -

    +

    Real-time transcription

    @@ -670,7 +667,7 @@ function TranscriptionSection() {
    -

    +

    Upload files

    @@ -754,8 +751,8 @@ function SummariesSection() { return (
    -
    -

    +
    +

    Your Notes+AI = Perfect Summary

    @@ -763,7 +760,7 @@ function SummariesSection() {
    -

    +

    While you take notes,{" "} Char listens and keeps track of everything that happens during the meeting. @@ -795,7 +792,7 @@ function SummariesSection() {

    -

    +

    After the meeting is over, {" "} @@ -886,7 +883,7 @@ function SummariesSection() {

    -

    +

    While you take notes,{" "} Char listens and keeps track of everything that happens during the meeting. @@ -918,7 +915,7 @@ function SummariesSection() {

    -

    +

    After the meeting is over, {" "} @@ -1018,9 +1015,9 @@ function SearchSection() { }} >

    -
    +
    -

    +

    Find anything instantly

    @@ -1851,11 +1848,11 @@ TrackProtectCell.displayName = "TrackProtectCell"; function SharingSection() { return (

    -
    +
    Coming Soon
    -

    Share notes

    +

    Share notes

    Collaborate seamlessly by sharing meeting notes, transcripts, and summaries with your team. @@ -1870,7 +1867,7 @@ function SharingSection() { icon="mdi:account-group" className="text-3xl text-stone-700" /> -

    +

    Control who can access

    @@ -1888,7 +1885,7 @@ function SharingSection() { icon="mdi:link-variant" className="text-3xl text-stone-700" /> -

    +

    Share instantly

    @@ -1906,7 +1903,7 @@ function SharingSection() { icon="mdi:shield-lock" className="text-3xl text-stone-700" /> -

    +

    Track and protect

    @@ -1928,7 +1925,7 @@ function SharingSection() { icon="mdi:account-group" className="text-3xl text-stone-700" /> -

    +

    Control who can access

    @@ -1946,7 +1943,7 @@ function SharingSection() { icon="mdi:link-variant" className="text-3xl text-stone-700" /> -

    +

    Share instantly

    @@ -1964,7 +1961,7 @@ function SharingSection() { icon="mdi:shield-lock" className="text-3xl text-stone-700" /> -

    +

    Track and protect

    @@ -1986,7 +1983,7 @@ function SharingSection() { icon="mdi:account-group" className="text-2xl text-stone-700" /> -

    +

    Control who can access

    @@ -2004,7 +2001,7 @@ function SharingSection() { icon="mdi:link-variant" className="text-2xl text-stone-700" /> -

    +

    Share instantly

    @@ -2022,7 +2019,7 @@ function SharingSection() { icon="mdi:shield-lock" className="text-2xl text-stone-700" /> -

    +

    Track and protect

    @@ -2083,11 +2080,11 @@ function FloatingPanelSection() { function FloatingPanelHeader() { return ( -
    +
    Coming Soon
    -

    +

    Floating panel for meetings

    @@ -2224,7 +2221,7 @@ function FloatingPanelTablet({ /> )}

    -

    +

    {tab.title}

    {tab.description}

    @@ -2317,7 +2314,7 @@ function FloatingPanelDesktop() { /> )}
    -

    +

    {tab.title}

    {tab.description}

    @@ -2424,7 +2421,7 @@ function FloatingPanelMobile({ function CTASection() { return (
    -
    +
    -

    +

    The complete AI notetaking solution

    diff --git a/apps/web/src/routes/_view/product/api.tsx b/apps/web/src/routes/_view/product/api.tsx index 29d551b65e..490db69e17 100644 --- a/apps/web/src/routes/_view/product/api.tsx +++ b/apps/web/src/routes/_view/product/api.tsx @@ -20,14 +20,11 @@ export const Route = createFileRoute("/_view/product/api")({ function Component() { return ( -

    -
    +
    +
    -
    -

    +
    +

    Char API

    diff --git a/apps/web/src/routes/_view/product/bot.tsx b/apps/web/src/routes/_view/product/bot.tsx index a6b4756d92..4b19e25708 100644 --- a/apps/web/src/routes/_view/product/bot.tsx +++ b/apps/web/src/routes/_view/product/bot.tsx @@ -26,11 +26,8 @@ const DRAGGABLE_ICONS = [ function Component() { return ( -

    -
    +
    +
    {DRAGGABLE_ICONS.map((icon, idx) => ( -
    -

    +
    +

    Char Bot

    diff --git a/apps/web/src/routes/_view/product/extensions.tsx b/apps/web/src/routes/_view/product/extensions.tsx index b6e179b080..298d3f930c 100644 --- a/apps/web/src/routes/_view/product/extensions.tsx +++ b/apps/web/src/routes/_view/product/extensions.tsx @@ -34,14 +34,11 @@ function Component() { ]; return ( -

    -
    +
    +
    -
    -

    +
    +

    Build Beyond the Defaults

    diff --git a/apps/web/src/routes/_view/product/flexible-ai.tsx b/apps/web/src/routes/_view/product/flexible-ai.tsx index 9395ffd26d..698c7087e6 100644 --- a/apps/web/src/routes/_view/product/flexible-ai.tsx +++ b/apps/web/src/routes/_view/product/flexible-ai.tsx @@ -23,11 +23,8 @@ export const Route = createFileRoute("/_view/product/flexible-ai")({ function Component() { return ( -

    -
    +
    +
    @@ -47,9 +44,9 @@ function Component() { function HeroSection() { return (
    -
    +
    -

    +

    Take Meeting Notes With
    AI of Your Choice @@ -80,16 +77,16 @@ function HeroSection() { function AISetupSection() { return (
    -
    -

    +

    +

    Pick your AI setup

    -

    - Char Cloud ($25/month) +

    + Char Cloud ($8/month)

    Managed service that works out of the box. No setup, no API keys, no @@ -101,7 +98,7 @@ function AISetupSection() { icon="mdi:key-variant" className="mb-4 text-3xl text-stone-700" /> -

    +

    Bring Your Own Key (Free)

    @@ -111,7 +108,7 @@ function AISetupSection() {

    -

    +

    Go fully local if you want to

    @@ -133,7 +130,7 @@ function LocalFeaturesSection() { className="shrink-0 text-3xl text-stone-700" />

    -

    +

    Local transcription with Whisper

    @@ -145,7 +142,7 @@ function LocalFeaturesSection() {

    -

    +

    Local LLM inference

    @@ -162,17 +159,17 @@ function LocalFeaturesSection() { function SwitchSection() { return (

    -
    -

    +

    +

    Switch providers anytime

    -

    +

    Your notes aren't locked to any AI provider.

    -

    +

    Start with Cloud

    @@ -180,7 +177,7 @@ function SwitchSection() {

    -

    +

    Change based on needs

    @@ -189,7 +186,7 @@ function SwitchSection() {

    -

    +

    Re-process meetings

    @@ -197,7 +194,7 @@ function SwitchSection() {

    -

    +

    Data never moves

    @@ -212,8 +209,8 @@ function SwitchSection() { function BenchmarkSection() { return (

    -
    -

    +
    +

    Confused which AI model to choose?

    @@ -240,8 +237,8 @@ function FAQSection() { return (

    -
    -

    +
    +

    Frequently asked questions

    diff --git a/apps/web/src/routes/_view/product/integrations.tsx b/apps/web/src/routes/_view/product/integrations.tsx index 8af17f9e3d..fa33363699 100644 --- a/apps/web/src/routes/_view/product/integrations.tsx +++ b/apps/web/src/routes/_view/product/integrations.tsx @@ -87,17 +87,14 @@ function IntegrationsGrid() { function Component() { return ( -
    -
    +
    +
    -
    -

    +
    +

    Integrations & Workflows

    diff --git a/apps/web/src/routes/_view/product/local-ai.tsx b/apps/web/src/routes/_view/product/local-ai.tsx index efc12527a1..e3142a0db7 100644 --- a/apps/web/src/routes/_view/product/local-ai.tsx +++ b/apps/web/src/routes/_view/product/local-ai.tsx @@ -37,11 +37,8 @@ function Component() { const heroInputRef = useRef(null); return ( -

    -
    +
    +
    @@ -63,9 +60,9 @@ function Component() { function HeroSection() { return (
    -
    +
    -

    +

    AI that runs
    on your device @@ -107,8 +104,8 @@ function HeroSection() { function WhyLocalAISection() { return (
    -
    -

    +

    +

    Why local AI

    @@ -118,7 +115,7 @@ function WhyLocalAISection() { icon="mdi:shield-lock" className="mb-4 text-3xl text-stone-700" /> -

    +

    Complete privacy

    @@ -132,7 +129,7 @@ function WhyLocalAISection() { icon="mdi:lightning-bolt" className="mb-4 text-3xl text-stone-700" /> -

    +

    Lightning fast

    @@ -142,7 +139,7 @@ function WhyLocalAISection() {

    -

    +

    Works offline

    @@ -155,7 +152,7 @@ function WhyLocalAISection() { icon="mdi:credit-card-off" className="mb-4 text-3xl text-stone-700" /> -

    +

    No usage limits

    @@ -171,8 +168,8 @@ function WhyLocalAISection() { function ComparisonSection() { return (

    -
    -

    +

    +

    Local AI vs. Cloud AI

    @@ -183,7 +180,7 @@ function ComparisonSection() { icon="mdi:cloud-upload" className="text-2xl text-neutral-400" /> -

    +

    Cloud AI Services

    @@ -235,7 +232,7 @@ function ComparisonSection() {
    -

    Char Local AI

    +

    Char Local AI