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() | |
| Docs index by meaning: passages of every page and project doc, embedded on save and recalled for agents; hybrid search for people | 27 | ? await docs.search(params.owner.toLowerCase(), viewer, { query: q, space_id: space, project, limit: 50, mode: "hybrid" }).catch(() => null) |
| Docs: a workspace knowledge base people and agents write together | 28 | : null; |
| 29 | return { q, space, project, hits: found?.ok ? found.value : q.trim() ? null : ([] as DocSearchHit[]) }; | |
| 30 | } | |
| 31 | ||
| Docs index by meaning: passages of every page and project doc, embedded on save and recalled for agents; hybrid search for people | 32 | /** |
| 33 | * Every page the viewer can read, and the projects' docs shown in Docs, | |
| 34 | * by words and by meaning at once; by space and by project. Each hit shows | |
| 35 | * the passage that matched, under its heading. | |
| 36 | */ | |
| Docs: a workspace knowledge base people and agents write together | 37 | export default function DocsSearch({ loaderData, params }: Route.ComponentProps) { |
| 38 | const slug = params.owner.toLowerCase(); | |
| 39 | const { q, space, project, hits } = loaderData; | |
| 40 | const layout = useDocsData(); | |
| 41 | const submit = useSubmit(); | |
| 42 | const spaces = layout?.sidebar?.spaces ?? []; | |
| 43 | const projects = [...new Set(spaces.flatMap((s) => s.projects))].sort(); | |
| 44 | const refresh = () => submit(document.getElementById("docs-search") as HTMLFormElement); | |
| 45 | return ( | |
| 46 | <div className="mx-auto max-w-3xl"> | |
| 47 | <h1 className="text-xl font-semibold tracking-tight">Search docs</h1> | |
| 48 | <Form method="get" id="docs-search" className="mt-4 flex flex-wrap gap-2"> | |
| 49 | <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"> | |
| 50 | <Search size={16} className="text-faint" aria-hidden="true" /> | |
| Docs index by meaning: passages of every page and project doc, embedded on save and recalled for agents; hybrid search for people | 51 | <input name="q" defaultValue={q} autoFocus placeholder="Words, or a question" aria-label="Search" className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint" /> |
| Docs: a workspace knowledge base people and agents write together | 52 | </label> |
| 53 | <SelectField | |
| 54 | name="space" | |
| 55 | aria-label="Space" | |
| 56 | value={space ?? ""} | |
| 57 | afterChange={refresh} | |
| 58 | options={[{ value: "", label: "Every space" }, ...spaces.map((s) => ({ value: s.id, label: s.name }))]} | |
| 59 | className="h-10 min-w-40" | |
| 60 | /> | |
| 61 | {projects.length > 0 && ( | |
| 62 | <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" /> | |
| 63 | )} | |
| 64 | </Form> | |
| 65 | <div className="mt-6"> | |
| 66 | {hits === null ? ( | |
| 67 | <EmptyState title="Search didn't answer">Try again in a moment.</EmptyState> | |
| 68 | ) : !q.trim() ? ( | |
| Docs index by meaning: passages of every page and project doc, embedded on save and recalled for agents; hybrid search for people | 69 | <p className="text-sm text-muted"> |
| 70 | Search every space you can read, and the projects' docs shown in Docs, by their words and by what they mean: ask a question and the passage that answers it comes up even when it words it differently. Agents recall from | |
| 71 | Docs the same way, and only from what everyone they answer can read. | |
| 72 | </p> | |
| Docs: a workspace knowledge base people and agents write together | 73 | ) : hits.length === 0 ? ( |
| 74 | <EmptyState title="Nothing found">No page you can read matches “{q}”.</EmptyState> | |
| 75 | ) : ( | |
| 76 | <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface"> | |
| 77 | {hits.map((hit) => ( | |
| 78 | <li key={hit.id}> | |
| 79 | <Link to={hit.path} className="block px-4 py-3 transition-colors hover:bg-raised"> | |
| 80 | <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 | 81 | {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 | 82 | <span className="truncate text-sm font-medium">{hit.title || "Untitled"}</span> |
| 83 | <span className="ml-auto shrink-0 text-xs text-faint"> | |
| 84 | {hit.space_name} · <TimeAgo at={hit.updated_at} /> | |
| 85 | </span> | |
| 86 | </span> | |
| Docs index by meaning: passages of every page and project doc, embedded on save and recalled for agents; hybrid search for people | 87 | {hit.heading && <span className="mt-0.5 block truncate text-xs font-medium text-muted">{hit.heading}</span>} |
| Docs: a workspace knowledge base people and agents write together | 88 | <span className="mt-1 line-clamp-2 block text-xs leading-relaxed text-muted"> |
| 89 | {snippetParts(hit.snippet).map((part, i) => | |
| 90 | part.match ? ( | |
| 91 | <mark key={i} className="rounded bg-accent/20 px-0.5 text-fg"> | |
| 92 | {part.text} | |
| 93 | </mark> | |
| 94 | ) : ( | |
| 95 | <span key={i}>{part.text}</span> | |
| 96 | ), | |
| 97 | )} | |
| 98 | </span> | |
| 99 | {hit.projects.length > 0 && <span className="mt-1 block text-[0.6875rem] text-faint">{hit.projects.join(" · ")}</span>} | |
| 100 | </Link> | |
| 101 | </li> | |
| 102 | ))} | |
| 103 | </ul> | |
| 104 | )} | |
| 105 | </div> | |
| 106 | <p className="mt-4 text-xs text-faint"> | |
| 107 | <Link to={`/${slug}/-/docs`} className="hover:text-fg"> | |
| 108 | Back to Docs | |
| 109 | </Link> | |
| 110 | </p> | |
| 111 | </div> | |
| 112 | ); | |
| 113 | } |