Skip to content

Commit

Permalink
[DOC] Docs search
Browse files Browse the repository at this point in the history
  • Loading branch information
itaismith committed Dec 20, 2024
2 parents e98f930 + 497df93 commit 133032e
Show file tree
Hide file tree
Showing 14 changed files with 668 additions and 1,145 deletions.
81 changes: 81 additions & 0 deletions docs/docs.trychroma.com/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { NextResponse } from "next/server";
import { ChromaClient } from "chromadb";
// @ts-ignore
import { Collection } from "chromadb/src/Collection";

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const query = searchParams.get("q");

if (!query) {
return NextResponse.json(
{ error: "Query parameter is required" },
{ status: 400 },
);
}

const chromaClient = new ChromaClient({
path: "https://api.trychroma.com:8000",
auth: {
provider: "token",
credentials: process.env.CHROMA_CLOUD_API_KEY,
tokenHeaderType: "X_CHROMA_TOKEN",
},
tenant: process.env.CHROMA_CLOUD_TENANT,
database: "docs",
});

const collection: Collection = await chromaClient.getOrCreateCollection({
name: "docs-content",
});

let results: {
distance: number;
title: string;
pageTitle: string;
pageUrl: string;
}[] = [];

const queryResults = await collection.query({
queryTexts: [query],
include: ["metadatas"],
where:
results.length > 0
? { pageTitle: { $nin: results.map((r) => r.pageTitle) } }
: undefined,
});

results.push(
...queryResults.metadatas[0].map(
(
m: {
pageTitle: string;
title: string;
page: string;
section: string;
subsection?: string;
},
index: number,
) => {
return {
title: m.title,
pageTitle: m.pageTitle,
pageUrl: m.subsection
? `/${m.section}/${m.subsection}/${m.page}${m.pageTitle !== m.title ? `#${m.title.replaceAll(" ", "-").replaceAll("_", "-").toLowerCase()}` : ""}`
: `/${m.section}/${m.page}${m.pageTitle !== m.title ? `#${m.title.replaceAll(" ", "-").replaceAll("_", "-").toLowerCase()}` : ""}`,
};
},
),
);

results = Array.from(
new Map(results.map((item) => [item.title, item])).values(),
);

return NextResponse.json(results);
} catch (error) {
console.log(error);
return NextResponse.json({ error: "Search failed" }, { status: 500 });
}
}
4 changes: 2 additions & 2 deletions docs/docs.trychroma.com/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import GithubLink from "@/components/header/github-link";
import XLink from "@/components/header/x-link";
import DiscordLink from "@/components/header/discord-link";
import Link from "next/link";
import SearchBox from "@/components/header/search-box";
import SearchDocs from "@/components/header/search-docs";

const Header: React.FC = () => {
return (
Expand All @@ -14,9 +14,9 @@ const Header: React.FC = () => {
<Link href="/">
<Logo />
</Link>
<SearchBox />
</div>
<div className="flex items-center justify-between gap-2">
<SearchDocs />
<DiscordLink />
<GithubLink />
<XLink />
Expand Down
145 changes: 145 additions & 0 deletions docs/docs.trychroma.com/components/header/search-docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"use client";

import React, { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogTrigger } from "../ui/dialog";
import UIButton from "@/components/ui/ui-button";
import { Cross2Icon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import _ from "lodash";
import { Input } from "@/components/ui/input";
import ChromaIcon from "../../public/chroma-icon.svg";
import { AlertTriangleIcon, ArrowRight, Loader } from "lucide-react";
import Link from "next/link";

const SearchDocs: React.FC = () => {
const [query, setQuery] = useState("");
const [results, setResults] = useState<
{ title: string; pageTitle: string; pageUrl: string }[]
>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const debouncedSearch = _.debounce(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}

try {
setIsLoading(true);
setError(null);

const response = await fetch(
`/api/search?q=${encodeURIComponent(searchQuery)}`,
);

if (!response.ok) {
throw new Error("Search failed");
}

const data = await response.json();
setResults(data);
console.log(data);
} catch (err) {
setError("Failed to perform search");
setResults([]);
} finally {
setIsLoading(false);
}
}, 300);

useEffect(() => {
debouncedSearch(query);

return () => {
debouncedSearch.cancel();
};
}, [query]);

return (
<Dialog>
<DialogTrigger asChild>
<UIButton className="lex items-center gap-2 p-[0.35rem] px-3 text-xs">
<MagnifyingGlassIcon className="w-4 h-4" />
<p>Search...</p>
</UIButton>
</DialogTrigger>
<DialogContent className="h-96 flex flex-col gap-0 sm:rounded-none p-0">
<div className="relative py-2 px-[3px] h-fit border-b-[1px] border-black dark:border-gray-300">
<div className="flex flex-col gap-0.5">
{[...Array(7)].map((_, index) => (
<div
key={index}
className="w-full h-[1px] bg-black dark:bg-gray-300"
/>
))}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 px-2 py-1 bg-white dark:bg-gray-950 font-mono">
Search Docs
</div>
<div className="absolute right-4 top-[6px] px-1 bg-white dark:bg-gray-950">
<DialogPrimitive.Close className="flex items-center justify-center bg-white dark:bg-gray-950 border-[1px] border-black disabled:pointer-events-none ">
<Cross2Icon className="h-5 w-5" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
</div>
</div>
<div className="relativ px-4 my-4">
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full p-2 border border-black rounded-none"
/>
</div>
<div className="flex-grow overflow-y-scroll px-4">
{isLoading && (
<div className="flex items-center justify-center w-full h-full">
<Loader className="w-5 h-5 animate-spin" />
</div>
)}
{error && (
<div className="flex flex-col gap-2 items-center justify-center w-full h-full">
<AlertTriangleIcon className="w-5 h-5 text-red-500" />
<p className="text-xs">
Failed to fetch results. Try again later
</p>
</div>
)}
{!isLoading && !error && (
<div className="flex flex-col gap-2 pb-10">
{results.map((result, index) => (
<Link
key={`result-${index}`}
href={result.pageUrl}
onClick={() => setQuery("")}
>
<DialogPrimitive.Close className="flex justify-between items-center w-full text-start p-3 border-[1.5px] h-16 hover:border-black dark:hover:border-blue-300 cursor-pointer">
<div className="flex flex-col gap-1">
<p className="text-sm font-semibold">
{result.title || result.pageTitle}
</p>
{result.title && result.title !== result.pageTitle && (
<p className="text-xs">{result.pageTitle}</p>
)}
</div>
<ArrowRight className="w-5 h-5" />
</DialogPrimitive.Close>
</Link>
))}
</div>
)}
</div>
<div className="flex justify-end py-2 px-4 border-t border-black">
<div className="flex items-center gap-2">
<ChromaIcon className="h-7 w-7" />
<p className="text-xs font-semibold">Powered by Chroma Cloud</p>
</div>
</div>
</DialogContent>
</Dialog>
);
};

export default SearchDocs;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const generateId = (content: React.ReactNode): string => {
if (typeof content === "string") {
return content
.toLowerCase()
.replaceAll("_", "-")
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.trim();
Expand Down
117 changes: 117 additions & 0 deletions docs/docs.trychroma.com/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"use client";

import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils";
import { Cross2Icon } from "@radix-ui/react-icons";

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogClose = DialogPrimitive.Close;

const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-zinc-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-zinc-800 dark:bg-zinc-950",
className,
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";

const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
22 changes: 22 additions & 0 deletions docs/docs.trychroma.com/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react"

import { cn } from "@/lib/utils"

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-zinc-200 bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-zinc-950 placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-zinc-800 dark:file:text-zinc-50 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"

export { Input }
Loading

0 comments on commit 133032e

Please sign in to comment.