Files

126 lines
4.6 KiB
TypeScript

// The knowledge module's page, as the one server component the web shell mounts under
// `/<org>/<workspace>/<project>/knowledge`. Every read is a procedure through the same caller the
// API uses; every write is a client component that calls a procedure through the shell's
// transport.
//
// The page renders three things separately on purpose: the service's health (the module flag and
// the search service's readiness), the indexing indicator (the per-document progress states), and
// the search itself — "indexing" is not "down", and the page never conflates them.
import type { Container, TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import type { ProjectPage } from "@conusai-kit/ui"
import type { ReactElement } from "react"
import {
type CollectionView,
collectionList,
type DocumentView,
documentList,
knowledgeStatus,
} from "../contract/index.js"
import { IndexForm, type IndexFormLabels } from "./index-form.js"
import { searchFormLabels, serviceStateLabel, statusLabels } from "./labels.js"
import { SearchForm } from "./search-form.js"
export type KnowledgePage = ProjectPage<TransportCaller, I18n, Container>
const indexFormLabels = (
t: (key: string, values?: Record<string, string | number>) => string,
failed: string,
): IndexFormLabels => ({
heading: t("upload.heading"),
label: t("upload.label"),
hint: t("upload.hint"),
submit: t("upload.submit"),
busy: t("upload.busy"),
done: t("upload.done"),
failed,
})
const documentStatusKey: Record<DocumentView["status"], string> = {
queued: "documents.statusQueued",
indexing: "documents.statusIndexing",
indexed: "documents.statusIndexed",
failed: "documents.statusFailed",
cancelled: "documents.statusCancelled",
}
export const knowledgePage: KnowledgePage = async ({ slugs, caller, i18n }): Promise<ReactElement> => {
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`knowledge.${key}`, values)
const failed = i18n.message("app.failed")
const [{ collections }, status] = await Promise.all([
caller.call(collectionList, slugs),
caller.call(knowledgeStatus, slugs),
])
const documentsByName = new Map<string, DocumentView[]>()
await Promise.all(
collections.map(async (collection: CollectionView) => {
const answer = await caller.call(documentList, { ...slugs, collectionName: collection.name })
documentsByName.set(collection.name, answer.documents)
}),
)
const labels = statusLabels(t, status)
return (
<>
<h1>{t("title")}</h1>
<p>{t("intro")}</p>
<section aria-labelledby="knowledge-status">
<h2 id="knowledge-status">{labels.heading}</h2>
{status.enabled ? (
<dl>
<dt>{labels.state}</dt>
<dd>{serviceStateLabel(t, status.service.state)}</dd>
{status.service.reasons.length > 0 ? (
<>
<dt>{labels.reasons}</dt>
<dd>
<ul>
{status.service.reasons.map((reason) => (
<li key={reason}>{reason}</li>
))}
</ul>
</dd>
</>
) : null}
<dt>{labels.indexing}</dt>
<dd>{labels.documents}</dd>
</dl>
) : (
<p role="status">{labels.flagOff}</p>
)}
</section>
{status.enabled
? collections.map((collection) => (
<section key={collection.name} aria-labelledby={`collection-${collection.name}`}>
<h2 id={`collection-${collection.name}`}>{collection.name}</h2>
<IndexForm slugs={slugs} collectionName={collection.name} labels={indexFormLabels(t, failed)} />
<SearchForm slugs={slugs} collectionName={collection.name} labels={searchFormLabels(t, failed)} />
<table>
<caption>{t("documents.heading")}</caption>
<thead>
<tr>
<th scope="col">{t("documents.file")}</th>
<th scope="col">{t("documents.status")}</th>
<th scope="col">{t("documents.error")}</th>
</tr>
</thead>
<tbody>
{(documentsByName.get(collection.name) ?? []).map((document) => (
<tr key={document.documentId}>
<td>{document.filename}</td>
<td>{t(documentStatusKey[document.status])}</td>
<td>{document.error ?? ""}</td>
</tr>
))}
</tbody>
</table>
</section>
))
: null}
</>
)
}