208 lines
7.6 KiB
TypeScript
208 lines
7.6 KiB
TypeScript
// The knowledge module's page, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web
|
|
// shell runs on the server under `/<org>/<workspace>/<project>/knowledge`, and the view of what it
|
|
// returned. Every read is a procedure through the same caller the API uses, and every label is translated
|
|
// in the loader; 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, notFound, type TransportCaller } from "@conusai-kit/core"
|
|
import type { I18n } from "@conusai-kit/i18n"
|
|
import { type ProjectPage, type ProjectPageViewProps, projectPage } from "@conusai-kit/ui"
|
|
import { FactList, Page, PageHeader, PageSection } from "@conusai-kit/ui/patterns"
|
|
import { Alert, AlertDescription } from "@conusai-kit/ui/primitives"
|
|
import type { ReactElement } from "react"
|
|
import {
|
|
autoIndexGet,
|
|
type CollectionView,
|
|
collectionList,
|
|
type DocumentView,
|
|
documentList,
|
|
knowledgeStatus,
|
|
} from "../contract/index.js"
|
|
import { AutoIndexForm, type AutoIndexFormLabels } from "./auto-index-form.js"
|
|
import { IndexForm, type IndexFormLabels } from "./index-form.js"
|
|
import {
|
|
failureLabels,
|
|
type RefusalNotice,
|
|
refusalLabels,
|
|
type SearchFormLabels,
|
|
type StatusLabels,
|
|
searchFormLabels,
|
|
serviceStateLabel,
|
|
statusLabels,
|
|
} from "./labels.js"
|
|
import { CollectionDocuments, type DocumentListLabels } from "./live-documents.js"
|
|
import { SearchForm } from "./search-form.js"
|
|
|
|
/** One collection and its documents: the seed of its live list. */
|
|
export interface KnowledgeCollectionRow {
|
|
readonly name: string
|
|
readonly documents: readonly DocumentView[]
|
|
}
|
|
|
|
export interface KnowledgePageData {
|
|
readonly title: string
|
|
readonly intro: string
|
|
readonly status: {
|
|
readonly enabled: boolean
|
|
readonly labels: StatusLabels
|
|
/** The service's state in the reader's language. */
|
|
readonly state: string
|
|
readonly reasons: readonly string[]
|
|
/** Why the service will not serve this deployment, when that is the answer. */
|
|
readonly refusal?: RefusalNotice
|
|
}
|
|
readonly collections: readonly KnowledgeCollectionRow[]
|
|
readonly documents: DocumentListLabels
|
|
readonly index: IndexFormLabels
|
|
readonly search: SearchFormLabels
|
|
/** The project's automatic PDF indexing: the collection it fills, or null when it is off. */
|
|
readonly autoIndex: { readonly current: string | null; readonly labels: AutoIndexFormLabels }
|
|
}
|
|
|
|
export type KnowledgePage = ProjectPage<TransportCaller, I18n, Container, KnowledgePageData>
|
|
|
|
const indexFormLabels = (t: (key: string, values?: Record<string, string | number>) => 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"),
|
|
})
|
|
|
|
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 = projectPage({
|
|
async load({ slugs, segments, caller, i18n }): Promise<KnowledgePageData> {
|
|
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`knowledge.${key}`, values)
|
|
if (segments.length > 0) throw notFound("no such page")
|
|
|
|
const [{ collections }, status, auto] = await Promise.all([
|
|
caller.call(collectionList, slugs),
|
|
caller.call(knowledgeStatus, slugs),
|
|
caller.call(autoIndexGet, slugs),
|
|
])
|
|
const rows = await Promise.all(
|
|
collections.map(async (collection: CollectionView): Promise<KnowledgeCollectionRow> => {
|
|
const { documents } = await caller.call(documentList, { ...slugs, collectionName: collection.name })
|
|
return { name: collection.name, documents }
|
|
}),
|
|
)
|
|
|
|
return {
|
|
title: t("title"),
|
|
intro: t("intro"),
|
|
status: {
|
|
enabled: status.enabled,
|
|
labels: statusLabels(t, status),
|
|
state: serviceStateLabel(t, status.service.state),
|
|
reasons: status.service.reasons,
|
|
...(status.service.refusal === undefined
|
|
? {}
|
|
: {
|
|
refusal: {
|
|
code: status.service.refusal,
|
|
text: refusalLabels(t)[status.service.refusal],
|
|
},
|
|
}),
|
|
},
|
|
collections: rows,
|
|
documents: {
|
|
caption: t("documents.heading"),
|
|
file: t("documents.file"),
|
|
status: t("documents.status"),
|
|
error: t("documents.error"),
|
|
statuses: Object.fromEntries(
|
|
Object.entries(documentStatusKey).map(([status, key]) => [status, t(key)]),
|
|
) as DocumentListLabels["statuses"],
|
|
failures: failureLabels(t),
|
|
},
|
|
index: indexFormLabels(t),
|
|
search: searchFormLabels(t),
|
|
autoIndex: {
|
|
current: auto.collectionName,
|
|
labels: {
|
|
heading: t("autoIndex.heading"),
|
|
hint: t("autoIndex.hint"),
|
|
label: t("autoIndex.label"),
|
|
off: t("autoIndex.off"),
|
|
submit: t("autoIndex.submit"),
|
|
busy: t("autoIndex.busy"),
|
|
done: t("autoIndex.done"),
|
|
},
|
|
},
|
|
}
|
|
},
|
|
|
|
View({ slugs, locale, data }: ProjectPageViewProps<KnowledgePageData>): ReactElement {
|
|
const { labels, enabled, state, reasons, refusal } = data.status
|
|
return (
|
|
<Page>
|
|
<PageHeader title={data.title} description={data.intro} />
|
|
<PageSection title={labels.heading} variant="card">
|
|
{refusal ? (
|
|
<Alert variant="warning" role="status" data-state={refusal.code}>
|
|
<AlertDescription>{refusal.text}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
{enabled ? (
|
|
<FactList
|
|
facts={[
|
|
{ label: labels.state, value: state },
|
|
...(reasons.length > 0
|
|
? [
|
|
{
|
|
label: labels.reasons,
|
|
value: (
|
|
<ul>
|
|
{reasons.map((reason) => (
|
|
<li key={reason}>{reason}</li>
|
|
))}
|
|
</ul>
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
{ label: labels.indexing, value: labels.documents },
|
|
]}
|
|
/>
|
|
) : (
|
|
<p role="status">{labels.flagOff}</p>
|
|
)}
|
|
</PageSection>
|
|
{enabled && data.collections.length > 0 ? (
|
|
<AutoIndexForm
|
|
slugs={slugs}
|
|
collections={data.collections.map((collection) => collection.name)}
|
|
current={data.autoIndex.current}
|
|
labels={data.autoIndex.labels}
|
|
/>
|
|
) : null}
|
|
{enabled
|
|
? data.collections.map((collection) => (
|
|
<PageSection key={collection.name} title={collection.name}>
|
|
<IndexForm slugs={slugs} collectionName={collection.name} labels={data.index} />
|
|
<SearchForm slugs={slugs} collectionName={collection.name} labels={data.search} locale={locale} />
|
|
<CollectionDocuments
|
|
slugs={slugs}
|
|
collectionName={collection.name}
|
|
seed={collection.documents}
|
|
labels={data.documents}
|
|
/>
|
|
</PageSection>
|
|
))
|
|
: null}
|
|
</Page>
|
|
)
|
|
},
|
|
})
|