Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Docs: a workspace knowledge base people and agents write together | 1 | import type { DocSearchHit } from "@g1t/contracts"; |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 2 | import { FolderGit2, Search } from "lucide-react"; |
| Docs: a workspace knowledge base people and agents write together | 3 | import { Form, Link, data, useSubmit } from "react-router"; |
| 4 | ||
| 5 | import type { Route } from "./+types/search"; | |
| 6 | import { useDocsData } from "../../../components/docs/actions"; | |
| 7 | import { PageIcon } from "../../../components/docs/sidebar"; | |
| 8 | import { EmptyState, TimeAgo } from "../../../components/ui"; | |
| 9 | import { SelectField } from "../../../components/ui/select"; | |
| 10 | import { snippetParts } from "../../../lib/docs"; | |
| 11 | import { page } from "../../../lib/meta"; | |
| 12 | import { docs } from "../../../lib/services.server"; | |
| 13 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 14 | ||
| 15 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | return page(args, { title: `Search docs · ${params.owner} · g1t` }); | |
| 17 | } | |
| 18 | ||
| 19 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 20 | const viewer = requireUser(context, request); | |
| 21 | if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 }); | |
| 22 | const url = new URL(request.url); | |
| 23 | const q = url.searchParams.get("q") ?? ""; | |
| 24 | const space = url.searchParams.get("space") || null; | |
| 25 | const project = url.searchParams.get("project") || null; | |
| 26 | const found = q.trim() | |
| 27 | ? await docs.search(params.owner.toLowerCase(), viewer, { query: q, space_id: space, project, limit: 50 }).catch(() => null) | |
| 28 | : null; | |
| 29 | return { q, space, project, hits: found?.ok ? found.value : q.trim() ? null : ([] as DocSearchHit[]) }; | |
| 30 | } | |
| 31 | ||
| 32 | /** Full text over every page the viewer can read; by space and by project. */ | |
| 33 | export default function DocsSearch({ loaderData, params }: Route.ComponentProps) { | |
| 34 | const slug = params.owner.toLowerCase(); | |
| 35 | const { q, space, project, hits } = loaderData; | |
| 36 | const layout = useDocsData(); | |
| 37 | const submit = useSubmit(); | |
| 38 | const spaces = layout?.sidebar?.spaces ?? []; | |
| 39 | const projects = [...new Set(spaces.flatMap((s) => s.projects))].sort(); | |
| 40 | const refresh = () => submit(document.getElementById("docs-search") as HTMLFormElement); | |
| 41 | return ( | |
| 42 | <div className="mx-auto max-w-3xl"> | |
| 43 | <h1 className="text-xl font-semibold tracking-tight">Search docs</h1> | |
| 44 | <Form method="get" id="docs-search" className="mt-4 flex flex-wrap gap-2"> | |
| 45 | <label className="flex h-10 min-w-0 grow items-center gap-2 rounded-md border border-line bg-surface px-3 focus-within:border-accent/60"> | |
| 46 | <Search size={16} className="text-faint" aria-hidden="true" /> | |
| 47 | <input name="q" defaultValue={q} autoFocus placeholder="Words in a title or a page" aria-label="Search" className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint" /> | |
| 48 | </label> | |
| 49 | <SelectField | |
| 50 | name="space" | |
| 51 | aria-label="Space" | |
| 52 | value={space ?? ""} | |
| 53 | afterChange={refresh} | |
| 54 | options={[{ value: "", label: "Every space" }, ...spaces.map((s) => ({ value: s.id, label: s.name }))]} | |
| 55 | className="h-10 min-w-40" | |
| 56 | /> | |
| 57 | {projects.length > 0 && ( | |
| 58 | <SelectField name="project" aria-label="Project" value={project ?? ""} afterChange={refresh} options={[{ value: "", label: "Any project" }, ...projects.map((p) => ({ value: p, label: p }))]} className="h-10 min-w-40" /> | |
| 59 | )} | |
| 60 | </Form> | |
| 61 | <div className="mt-6"> | |
| 62 | {hits === null ? ( | |
| 63 | <EmptyState title="Search didn't answer">Try again in a moment.</EmptyState> | |
| 64 | ) : !q.trim() ? ( | |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 65 | <p className="text-sm text-muted">Search titles and text across every space you can read, and the projects' docs shown in Docs. Agents search the same way, and only see what the people they answer can see.</p> |
| Docs: a workspace knowledge base people and agents write together | 66 | ) : hits.length === 0 ? ( |
| 67 | <EmptyState title="Nothing found">No page you can read matches “{q}”.</EmptyState> | |
| 68 | ) : ( | |
| 69 | <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface"> | |
| 70 | {hits.map((hit) => ( | |
| 71 | <li key={hit.id}> | |
| 72 | <Link to={hit.path} className="block px-4 py-3 transition-colors hover:bg-raised"> | |
| 73 | <span className="flex items-center gap-2"> | |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 74 | {hit.repo_file ? <FolderGit2 size={15} className="shrink-0 text-faint" aria-hidden="true" /> : <PageIcon icon={hit.icon} />} |
| Docs: a workspace knowledge base people and agents write together | 75 | <span className="truncate text-sm font-medium">{hit.title || "Untitled"}</span> |
| 76 | <span className="ml-auto shrink-0 text-xs text-faint"> | |
| 77 | {hit.space_name} · <TimeAgo at={hit.updated_at} /> | |
| 78 | </span> | |
| 79 | </span> | |
| 80 | <span className="mt-1 line-clamp-2 block text-xs leading-relaxed text-muted"> | |
| 81 | {snippetParts(hit.snippet).map((part, i) => | |
| 82 | part.match ? ( | |
| 83 | <mark key={i} className="rounded bg-accent/20 px-0.5 text-fg"> | |
| 84 | {part.text} | |
| 85 | </mark> | |
| 86 | ) : ( | |
| 87 | <span key={i}>{part.text}</span> | |
| 88 | ), | |
| 89 | )} | |
| 90 | </span> | |
| 91 | {hit.projects.length > 0 && <span className="mt-1 block text-[0.6875rem] text-faint">{hit.projects.join(" · ")}</span>} | |
| 92 | </Link> | |
| 93 | </li> | |
| 94 | ))} | |
| 95 | </ul> | |
| 96 | )} | |
| 97 | </div> | |
| 98 | <p className="mt-4 text-xs text-faint"> | |
| 99 | <Link to={`/${slug}/-/docs`} className="hover:text-fg"> | |
| 100 | Back to Docs | |
| 101 | </Link> | |
| 102 | </p> | |
| 103 | </div> | |
| 104 | ); | |
| 105 | } |