Merge remote-tracking branch 'upstream/dev' into fix/ui

This commit is contained in:
Anish Sarkar 2026-02-17 03:58:57 +05:30
commit f2a2872995
47 changed files with 1367 additions and 1493 deletions

View file

@ -126,38 +126,39 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
selection?.addRange(range);
}, []);
// Get plain text content (excluding chips)
// Get plain text content with inline mention tokens for chips.
// This preserves the original query structure sent to the backend/LLM.
const getText = useCallback((): string => {
if (!editorRef.current) return "";
let text = "";
const walker = document.createTreeWalker(
editorRef.current,
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
{
acceptNode: (node) => {
// Skip chip elements entirely
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
if (el.hasAttribute(CHIP_DATA_ATTR)) {
return NodeFilter.FILTER_REJECT; // Skip this subtree
}
return NodeFilter.FILTER_SKIP; // Continue into children
}
return NodeFilter.FILTER_ACCEPT;
},
}
);
let node: Node | null = walker.nextNode();
while (node) {
const extractText = (node: Node): string => {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
return node.textContent ?? "";
}
node = walker.nextNode();
}
return text.trim();
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
// Preserve mention chips as inline @title tokens.
if (element.hasAttribute(CHIP_DATA_ATTR)) {
const title = element.querySelector("[data-mention-title='true']")?.textContent?.trim();
if (title) {
return `@${title}`;
}
return "";
}
let result = "";
for (const child of Array.from(element.childNodes)) {
result += extractText(child);
}
return result;
}
return "";
};
return extractText(editorRef.current).trim();
}, []);
// Get all mentioned documents

View file

@ -10,9 +10,9 @@ import {
} from "@assistant-ui/react-markdown";
import { CheckIcon, CopyIcon } from "lucide-react";
import { type FC, memo, type ReactNode, useState } from "react";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import { InlineCitation } from "@/components/assistant-ui/inline-citation";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";

View file

@ -1,10 +1,10 @@
"use client";
import { useFeatureFlagVariantKey } from "@posthog/react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import React, { useEffect, useRef, useState } from "react";
import Balancer from "react-wrap-balancer";
import { WalkthroughScroll } from "@/components/ui/walkthrough-scroll";
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
@ -40,41 +40,37 @@ export function HeroSection() {
return (
<div
ref={parentRef}
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-20 md:px-8 md:py-40"
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:px-8 md:py-24"
>
<BackgroundGrids />
<CollisionMechanism
parentRef={parentRef}
beamOptions={{
initialX: -400,
translateX: 600,
duration: 7,
repeatDelay: 3,
}}
containerRef={containerRef}
parentRef={parentRef}
/>
<CollisionMechanism
parentRef={parentRef}
beamOptions={{
initialX: -200,
translateX: 800,
duration: 4,
repeatDelay: 3,
}}
containerRef={containerRef}
parentRef={parentRef}
/>
<CollisionMechanism
parentRef={parentRef}
beamOptions={{
initialX: 200,
translateX: 1200,
duration: 5,
repeatDelay: 3,
}}
containerRef={containerRef}
parentRef={parentRef}
/>
<CollisionMechanism
containerRef={containerRef}
parentRef={parentRef}
beamOptions={{
initialX: 400,
@ -106,34 +102,12 @@ export function HeroSection() {
<p className="relative z-50 mx-auto mt-0 max-w-lg px-4 text-center text-base/6 text-gray-600 dark:text-gray-200">
Then chat with it in real-time, even alongside your team.
</p>
<div className="mb-10 mt-8 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-20">
<div className="mb-6 mt-6 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-10">
<GetStartedButton />
<ContactSalesButton />
</div>
<div
ref={containerRef}
className="relative mx-auto max-w-7xl rounded-[32px] border border-neutral-200/50 bg-neutral-100 p-2 backdrop-blur-lg md:p-4 dark:border-neutral-700 dark:bg-neutral-800/50"
>
<div className="rounded-[24px] border border-neutral-200 bg-white p-2 dark:border-neutral-700 dark:bg-black">
{/* Light mode image */}
<Image
src="/homepage/main_demo.webp"
alt="header"
width={1920}
height={1080}
className="rounded-[20px] block dark:hidden"
unoptimized
/>
{/* Dark mode image */}
<Image
src="/homepage/main_demo.webp"
alt="header"
width={1920}
height={1080}
className="rounded-[20px] hidden dark:block"
unoptimized
/>
</div>
<div ref={containerRef} className="relative w-full">
<WalkthroughScroll />
</div>
</div>
);
@ -236,24 +210,23 @@ const BackgroundGrids = () => {
);
};
const CollisionMechanism = React.forwardRef<
HTMLDivElement,
{
containerRef: React.RefObject<HTMLDivElement | null>;
parentRef: React.RefObject<HTMLDivElement | null>;
beamOptions?: {
initialX?: number;
translateX?: number;
initialY?: number;
translateY?: number;
rotate?: number;
className?: string;
duration?: number;
delay?: number;
repeatDelay?: number;
};
}
>(({ parentRef, containerRef, beamOptions = {} }, ref) => {
const CollisionMechanism = ({
parentRef,
beamOptions = {},
}: {
parentRef: React.RefObject<HTMLDivElement | null>;
beamOptions?: {
initialX?: number;
translateX?: number;
initialY?: number;
translateY?: number;
rotate?: number;
className?: string;
duration?: number;
delay?: number;
repeatDelay?: number;
};
}) => {
const beamRef = useRef<HTMLDivElement>(null);
const [collision, setCollision] = useState<{
detected: boolean;
@ -264,14 +237,14 @@ const CollisionMechanism = React.forwardRef<
useEffect(() => {
const checkCollision = () => {
if (beamRef.current && containerRef.current && parentRef.current && !cycleCollisionDetected) {
if (beamRef.current && parentRef.current && !cycleCollisionDetected) {
const beamRect = beamRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const parentRect = parentRef.current.getBoundingClientRect();
const rightEdge = parentRect.right;
if (beamRect.bottom >= containerRect.top) {
const relativeX = beamRect.left - parentRect.left + beamRect.width / 2;
const relativeY = beamRect.bottom - parentRect.top;
if (beamRect.right >= rightEdge - 20) {
const relativeX = parentRect.width - 20;
const relativeY = beamRect.top - parentRect.top + beamRect.height / 2;
setCollision({
detected: true,
@ -288,7 +261,7 @@ const CollisionMechanism = React.forwardRef<
const animationInterval = setInterval(checkCollision, 100);
return () => clearInterval(animationInterval);
}, [cycleCollisionDetected, containerRef]);
}, [cycleCollisionDetected, parentRef]);
useEffect(() => {
if (collision.detected && collision.coordinates) {
@ -354,9 +327,7 @@ const CollisionMechanism = React.forwardRef<
</AnimatePresence>
</>
);
});
CollisionMechanism.displayName = "CollisionMechanism";
};
const Explosion = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {
const spans = Array.from({ length: 20 }, (_, index) => ({

View file

@ -0,0 +1,107 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
const useCases = [
{
title: "Search & Citation",
description: "Ask questions and get Perplexity-style cited responses from your knowledge base.",
src: "/homepage/hero_tutorial/BSNCGif.gif",
},
{
title: "Document Mention QNA",
description: "Mention specific documents in your queries for targeted answers.",
src: "/homepage/hero_tutorial/BQnaGif_compressed.gif",
},
{
title: "Report Generation",
description: "Generate and export reports in many formats.",
src: "/homepage/hero_tutorial/ReportGenGif_compressed.gif",
},
{
title: "Podcast Generation",
description: "Turn your knowledge into podcasts in under 20 seconds.",
src: "/homepage/hero_tutorial/PodcastGenGif.gif",
},
{
title: "Image Generation",
description: "Generate images directly from your conversations.",
src: "/homepage/hero_tutorial/ImageGenGif.gif",
},
];
function UseCaseCard({
title,
description,
src,
className,
}: {
title: string;
description: string;
src: string;
className?: string;
}) {
const { expanded, open, close } = useExpandedGif();
return (
<>
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`group overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-sm transition-shadow duration-300 hover:shadow-xl dark:border-neutral-700/60 dark:bg-neutral-900 ${className ?? ""}`}
>
<div
className="cursor-pointer overflow-hidden bg-neutral-50 p-2 dark:bg-neutral-950"
onClick={open}
>
<img
src={src}
alt={title}
className="w-full rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
/>
</div>
<div className="px-5 py-4">
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">{title}</h3>
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">{description}</p>
</div>
</motion.div>
<AnimatePresence>
{expanded && <ExpandedGifOverlay src={src} alt={title} onClose={close} />}
</AnimatePresence>
</>
);
}
export function UseCasesGrid() {
return (
<section className="relative mx-auto max-w-7xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
<div className="mb-6 text-center">
<h2 className="text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-white">
What You Can Do
</h2>
</div>
{/* First row: 2 larger cards */}
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
{useCases.slice(0, 2).map((useCase) => (
<UseCaseCard key={useCase.title} {...useCase} />
))}
</div>
{/* Second row: 3 equal cards */}
<div className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{useCases.slice(2).map((useCase) => (
<UseCaseCard key={useCase.title} {...useCase} />
))}
</div>
<p className="mt-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
And more coming soon.
</p>
</section>
);
}

View file

@ -68,7 +68,12 @@ interface WarningResult {
message?: string;
}
type DeleteNotionPageResult = InterruptResult | SuccessResult | ErrorResult | InfoResult | WarningResult;
type DeleteNotionPageResult =
| InterruptResult
| SuccessResult
| ErrorResult
| InfoResult
| WarningResult;
function isInterruptResult(result: unknown): result is InterruptResult {
return (
@ -341,22 +346,23 @@ function SuccessCard({ result }: { result: SuccessResult }) {
</p>
</div>
</div>
{(result.deleted_from_db || result.title) && (
<div className="space-y-2 px-4 py-3 text-xs">
{result.title && (
<div>
<span className="font-medium text-muted-foreground">Deleted page: </span>
<span>{result.title}</span>
</div>
)}
{result.deleted_from_db && (
<div className="pt-1">
<span className="text-green-600 dark:text-green-500">
Also removed from knowledge base
</span>
</div>
)}
</div>)}
{(result.deleted_from_db || result.title) && (
<div className="space-y-2 px-4 py-3 text-xs">
{result.title && (
<div>
<span className="font-medium text-muted-foreground">Deleted page: </span>
<span>{result.title}</span>
</div>
)}
{result.deleted_from_db && (
<div className="pt-1">
<span className="text-green-600 dark:text-green-500">
Also removed from knowledge base
</span>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,53 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useState } from "react";
function ExpandedGifOverlay({
src,
alt,
onClose,
}: {
src: string;
alt: string;
onClose: () => void;
}) {
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}, [onClose]);
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-100 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm sm:p-8"
onClick={onClose}
>
<motion.img
initial={{ scale: 0.85, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.85, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
src={src}
alt={alt}
className="max-h-[90vh] max-w-[90vw] rounded-2xl shadow-2xl"
onClick={(e) => e.stopPropagation()}
/>
</motion.div>
);
}
function useExpandedGif() {
const [expanded, setExpanded] = useState(false);
const open = useCallback(() => setExpanded(true), []);
const close = useCallback(() => setExpanded(false), []);
return { expanded, open, close };
}
export { ExpandedGifOverlay, useExpandedGif };

View file

@ -0,0 +1,123 @@
"use client";
import { AnimatePresence, motion, useScroll, useTransform } from "motion/react";
import { useRef } from "react";
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
const walkthroughSteps = [
{
step: 1,
title: "Login",
description: "Login to get started.",
src: "/homepage/hero_tutorial/LoginFlowGif.gif",
},
{
step: 2,
title: "Connect & Sync",
description: "Connect your connectors and sync. Enable periodic syncing to keep them updated.",
src: "/homepage/hero_tutorial/ConnectorFlowGif.gif",
},
{
step: 3,
title: "Upload Documents",
description: "While connectors index, upload your documents directly.",
src: "/homepage/hero_tutorial/DocUploadGif.gif",
},
];
function WalkthroughCard({
i,
step,
title,
description,
src,
progress,
range,
targetScale,
}: {
i: number;
step: number;
title: string;
description: string;
src: string;
progress: ReturnType<typeof useScroll>["scrollYProgress"];
range: [number, number];
targetScale: number;
}) {
const container = useRef<HTMLDivElement>(null);
const scale = useTransform(progress, range, [1, targetScale]);
const { expanded, open, close } = useExpandedGif();
return (
<>
<div
ref={container}
className="sticky top-0 flex items-center justify-center px-4 sm:px-6 lg:px-8"
>
<motion.div
style={{
scale,
top: `calc(10vh + ${i * 30}px)`,
}}
className="relative flex origin-top flex-col overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-xl sm:rounded-3xl dark:border-neutral-700/60 dark:bg-neutral-900
w-full max-w-[340px] sm:max-w-[520px] md:max-w-[680px] lg:max-w-[900px]"
>
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-neutral-900 text-xs font-semibold text-white sm:h-8 sm:w-8 sm:text-sm dark:bg-white dark:text-neutral-900">
{step}
</span>
<div className="min-w-0">
<h3 className="truncate text-sm font-semibold text-neutral-900 sm:text-base dark:text-white">
{title}
</h3>
<p className="hidden text-xs text-neutral-500 sm:block dark:text-neutral-400">
{description}
</p>
</div>
</div>
<div
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950"
onClick={open}
>
<img src={src} alt={title} className="w-full rounded-lg object-cover sm:rounded-xl" />
</div>
</motion.div>
</div>
<AnimatePresence>
{expanded && <ExpandedGifOverlay src={src} alt={title} onClose={close} />}
</AnimatePresence>
</>
);
}
function WalkthroughScroll() {
const container = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: container,
offset: ["start start", "end end"],
});
return (
<div
ref={container}
className="relative flex w-full flex-col items-center justify-center pb-[15vh] pt-[1vh] sm:pb-[18vh] sm:pt-[2vh]"
>
{walkthroughSteps.map((project, i) => {
const targetScale = Math.max(0.6, 1 - (walkthroughSteps.length - i - 1) * 0.05);
return (
<WalkthroughCard
key={`walkthrough_${i}`}
i={i}
{...project}
progress={scrollYProgress}
range={[i * (1 / walkthroughSteps.length), 1]}
targetScale={targetScale}
/>
);
})}
</div>
);
}
export { WalkthroughScroll, WalkthroughCard };