126 lines
4.6 KiB
TypeScript
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}
|
|
</>
|
|
)
|
|
}
|