@@ -0,0 +1,207 @@
|
||||
// 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>
|
||||
)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user