mirror of
https://github.com/langgenius/dify.git
synced 2026-09-03 23:47:16 +08:00
Import the committed KnowledgeFS snapshot dc4072ee302317145612087ce7440851dc329fd0 under knowledge-fs/ without its Git history, local IDE settings, or build artifacts.
3014 lines
102 KiB
TypeScript
3014 lines
102 KiB
TypeScript
import React from "react";
|
|
|
|
import {
|
|
type AdminAnswerTrace,
|
|
type AdminApiClient,
|
|
type AdminDocumentAsset,
|
|
type AdminGoldenQuestionList,
|
|
type AdminGraphTraversal,
|
|
type AdminHealthStatus,
|
|
type AdminKnowledgeFsCat,
|
|
type AdminKnowledgeFsDiff,
|
|
type AdminKnowledgeFsEntry,
|
|
type AdminKnowledgeFsGcDryRunReport,
|
|
type AdminKnowledgeFsGrep,
|
|
type AdminKnowledgeFsLeaseList,
|
|
type AdminKnowledgeFsList,
|
|
type AdminKnowledgeFsOpenNode,
|
|
type AdminKnowledgeFsStat,
|
|
type AdminKnowledgeFsTree,
|
|
type AdminKnowledgeFsTreeNode,
|
|
type AdminKnowledgeFsWrite,
|
|
type AdminKnowledgeFsckReport,
|
|
type AdminKnowledgeSpace,
|
|
type AdminKnowledgeSpaceManifest,
|
|
type AdminKnowledgeSpaceStagedCommitList,
|
|
type AdminKnowledgeSpaceStatus,
|
|
type AdminParseArtifact,
|
|
createAdminApiClient,
|
|
getAdminApiBase,
|
|
getAdminPublicApiBase,
|
|
} from "../lib/api-client";
|
|
import { formatBytes } from "../lib/display-format";
|
|
import { createDocumentHealthReport } from "../lib/document-health";
|
|
import { adminRowKey, graphEntityRowKey, graphRelationRowKey } from "../lib/graph-row-keys";
|
|
import { getAdminServerToken } from "../lib/server-auth";
|
|
import { createTraceComparison } from "../lib/trace-comparison";
|
|
import { createTraceSummary } from "../lib/trace-summary";
|
|
import { type NavEntry, SidebarNav } from "./components/sidebar-nav";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const fallbackWorkspaceOption = {
|
|
id: "workspace",
|
|
name: "workspace",
|
|
slug: "workspace",
|
|
} as const;
|
|
|
|
const defaultKnowledgeFsPath = "/knowledge/docs";
|
|
const knowledgeFsCommands = [
|
|
"ls",
|
|
"tree",
|
|
"cat",
|
|
"stat",
|
|
"grep",
|
|
"find",
|
|
"diff",
|
|
"open_node",
|
|
"write",
|
|
"append",
|
|
] as const;
|
|
|
|
interface AdminWorkspaceOption {
|
|
readonly id: string;
|
|
readonly name: string;
|
|
readonly slug: string;
|
|
}
|
|
|
|
type AdminKnowledgeFsCommand = (typeof knowledgeFsCommands)[number];
|
|
|
|
interface AdminKnowledgeFsCommandState {
|
|
readonly command: AdminKnowledgeFsCommand;
|
|
readonly depth: string;
|
|
readonly error?: string | undefined;
|
|
readonly findMetadataKey: string;
|
|
readonly findMetadataValue: string;
|
|
readonly findNameContains: string;
|
|
readonly findResourceType: string;
|
|
readonly grepQuery: string;
|
|
readonly limit: string;
|
|
readonly nodeId: string;
|
|
readonly writeText: string;
|
|
readonly result?:
|
|
| AdminKnowledgeFsCat
|
|
| AdminKnowledgeFsDiff
|
|
| AdminKnowledgeFsGrep
|
|
| AdminKnowledgeFsList
|
|
| AdminKnowledgeFsOpenNode
|
|
| AdminKnowledgeFsStat
|
|
| AdminKnowledgeFsTree
|
|
| AdminKnowledgeFsWrite
|
|
| undefined;
|
|
}
|
|
|
|
interface AdminHomeProps {
|
|
readonly searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
}
|
|
|
|
interface AdminHomeViewProps {
|
|
readonly healthItems: readonly AdminHealthMetric[];
|
|
readonly liveData: AdminLiveData;
|
|
readonly notice?: AdminNotice | undefined;
|
|
readonly queryResult?: AdminQueryResult | null | undefined;
|
|
readonly uploadResult?: AdminUploadResult | null | undefined;
|
|
readonly workspaceOptions: readonly AdminWorkspaceOption[];
|
|
}
|
|
|
|
interface AdminHealthMetric {
|
|
readonly badge: string;
|
|
readonly label: string;
|
|
readonly status: "fail" | "ok" | "warn";
|
|
readonly value: string;
|
|
}
|
|
|
|
interface AdminPublishReadiness {
|
|
readonly badge: string;
|
|
readonly elementCount: string;
|
|
readonly fileSize: string;
|
|
readonly nodeCount: string;
|
|
readonly parserStatus: string;
|
|
readonly qualityRisks: string;
|
|
readonly status: "fail" | "ready" | "review" | "warn";
|
|
readonly subtitle: string;
|
|
}
|
|
|
|
interface AdminLiveData {
|
|
readonly activeWorkspace: AdminWorkspaceOption;
|
|
readonly activeLeases?: AdminKnowledgeFsLeaseList | undefined;
|
|
readonly compareTrace?: AdminAnswerTrace | undefined;
|
|
readonly communityView?: AdminKnowledgeFsList | undefined;
|
|
readonly diff?: AdminKnowledgeFsDiff | undefined;
|
|
readonly entityId: string;
|
|
readonly entityView?: AdminKnowledgeFsList | undefined;
|
|
readonly errors: readonly string[];
|
|
readonly failedCommits?: AdminKnowledgeSpaceStagedCommitList | undefined;
|
|
readonly fsCommand: AdminKnowledgeFsCommandState;
|
|
readonly fsck?: AdminKnowledgeFsckReport | undefined;
|
|
readonly gcDryRun?: AdminKnowledgeFsGcDryRunReport | undefined;
|
|
readonly fsPath: string;
|
|
readonly graph?: AdminGraphTraversal | undefined;
|
|
readonly goldenQuestions?: AdminGoldenQuestionList | undefined;
|
|
readonly manifest?: AdminKnowledgeSpaceManifest | undefined;
|
|
readonly newPath: string;
|
|
readonly oldPath: string;
|
|
readonly publishReadiness?: AdminPublishReadiness | undefined;
|
|
readonly queryConflicts?: AdminKnowledgeFsList | undefined;
|
|
readonly queryEvidence?: AdminKnowledgeFsList | undefined;
|
|
readonly queryMissing?: AdminKnowledgeFsList | undefined;
|
|
readonly rootFs?: AdminKnowledgeFsList | undefined;
|
|
readonly status?: AdminKnowledgeSpaceStatus | undefined;
|
|
readonly topicView?: AdminKnowledgeFsList | undefined;
|
|
readonly trace?: AdminAnswerTrace | undefined;
|
|
}
|
|
|
|
interface AdminNotice {
|
|
readonly message: string;
|
|
readonly status: "error" | "success";
|
|
}
|
|
|
|
type AdminUploadResult =
|
|
| {
|
|
readonly documentId: string;
|
|
readonly filename: string;
|
|
readonly parserStatus: "failed" | "parsed" | "pending";
|
|
readonly sha256: string;
|
|
readonly sizeBytes: number;
|
|
readonly spaceId: string;
|
|
readonly status: "success";
|
|
readonly version: number;
|
|
}
|
|
| {
|
|
readonly error: string;
|
|
readonly status: "error";
|
|
};
|
|
|
|
type AdminQueryResult =
|
|
| {
|
|
readonly answer: string;
|
|
readonly citations: readonly string[];
|
|
readonly confidence: string;
|
|
readonly freshness: string;
|
|
readonly query: string;
|
|
readonly status: "success";
|
|
readonly traceId?: string | undefined;
|
|
}
|
|
| {
|
|
readonly error: string;
|
|
readonly status: "error";
|
|
};
|
|
|
|
interface Loaded<T> {
|
|
readonly error?: string | undefined;
|
|
readonly value?: T | undefined;
|
|
}
|
|
|
|
export default async function AdminHome({ searchParams }: AdminHomeProps) {
|
|
const resolvedSearchParams = await Promise.resolve(searchParams ?? {});
|
|
const [workspaceOptions, health] = await Promise.all([
|
|
loadAdminWorkspaceOptions(),
|
|
loadAdminHealth(),
|
|
]);
|
|
const uploadResult = parseUploadResult(resolvedSearchParams);
|
|
const queryResult = parseQueryResult(resolvedSearchParams);
|
|
const liveData = await loadAdminLiveData({
|
|
queryResult,
|
|
searchParams: resolvedSearchParams,
|
|
uploadResult,
|
|
workspaceOptions,
|
|
});
|
|
|
|
return (
|
|
<AdminHomeView
|
|
healthItems={createHealthItems(health)}
|
|
liveData={liveData}
|
|
notice={parseAdminNotice(resolvedSearchParams)}
|
|
queryResult={queryResult}
|
|
uploadResult={uploadResult}
|
|
workspaceOptions={workspaceOptions}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function AdminHomeView({
|
|
healthItems,
|
|
liveData,
|
|
notice,
|
|
queryResult = null,
|
|
uploadResult = null,
|
|
workspaceOptions,
|
|
}: AdminHomeViewProps) {
|
|
const apiBase = getAdminPublicApiBase();
|
|
const resolvedWorkspaceOptions =
|
|
workspaceOptions.length > 0 ? workspaceOptions : [fallbackWorkspaceOption];
|
|
const activeWorkspaceId = liveData.activeWorkspace.id;
|
|
const retrievalResult = createRetrievalResult(queryResult);
|
|
const publishReadiness = liveData.publishReadiness ?? createPublishReadiness(uploadResult);
|
|
const evaluationStats = createEvaluationStats(liveData.goldenQuestions);
|
|
const semanticViewStatus = createSemanticViewStatus(liveData);
|
|
|
|
return (
|
|
<main className="admin-shell">
|
|
<aside className="sidebar" aria-label="Admin navigation">
|
|
<div className="brand">
|
|
<strong>KnowledgeFS Admin</strong>
|
|
<span>Standalone console</span>
|
|
</div>
|
|
<SidebarNav
|
|
items={
|
|
[
|
|
{ label: "System health", href: "#health" },
|
|
{ label: "Control plane", href: "#control-plane" },
|
|
{ label: "FSCK", href: "#fsck" },
|
|
{ label: "GC", href: "#gc-staged-objects" },
|
|
{ label: "Upload intake", href: "#upload" },
|
|
{ label: "Retrieval workspace", href: "#retrieval" },
|
|
{ label: "KnowledgeFS", href: "#knowledge-fs" },
|
|
{ label: "Documents", href: `/documents/${encodeURIComponent(activeWorkspaceId)}` },
|
|
{ label: "Entity browser", href: "#entities" },
|
|
{ label: "Semantic views", href: "#semantic-views" },
|
|
{ label: "Document diff", href: "#document-diff" },
|
|
{ label: "Golden questions", href: "#golden-questions" },
|
|
{ label: "Evaluation dashboard", href: "#evaluation-dashboard" },
|
|
{ label: "Retrieval Studio", href: "#retrieval-studio" },
|
|
{ label: "Trace comparison", href: "#trace-comparison" },
|
|
{ label: "Failed diagnostics", href: "#failed-query-diagnostics" },
|
|
{ label: "Trace review", href: "#trace" },
|
|
] satisfies NavEntry[]
|
|
}
|
|
/>
|
|
</aside>
|
|
|
|
<section className="workspace">
|
|
<header className="topbar">
|
|
<div>
|
|
<h1>KnowledgeFS Admin</h1>
|
|
<p>Operational console for ingestion, retrieval, traces, and readiness.</p>
|
|
</div>
|
|
<div className="api-base" aria-label="API base">
|
|
<span>API base</span>
|
|
<code>{apiBase}</code>
|
|
</div>
|
|
</header>
|
|
|
|
{notice ? (
|
|
<section className={`result-card ${notice.status === "error" ? "fail" : ""}`}>
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>
|
|
{notice.status === "success" ? "Admin action complete" : "Admin action failed"}
|
|
</h3>
|
|
<small>{notice.message}</small>
|
|
</div>
|
|
<span className={`badge ${notice.status === "success" ? "ok" : "fail"}`}>
|
|
{notice.status}
|
|
</span>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
|
|
{liveData.errors.length > 0 ? (
|
|
<section className="result-card fail" aria-label="Admin data load warnings">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>Some live Admin data could not be loaded</h3>
|
|
<small>{liveData.errors.slice(0, 3).join("; ")}</small>
|
|
</div>
|
|
<span className="badge warn">partial</span>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
|
|
<section id="health" className="grid health-grid" aria-label="System health">
|
|
{healthItems.map((item) => (
|
|
<article className="metric" key={item.label}>
|
|
<span>{item.label}</span>
|
|
<strong>{item.value}</strong>
|
|
<em className={`badge ${item.status}`}>{item.badge}</em>
|
|
</article>
|
|
))}
|
|
</section>
|
|
|
|
<section className="grid work-grid" aria-label="Admin work areas">
|
|
<article id="control-plane" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Control plane</h2>
|
|
<small>{liveData.activeWorkspace.name}</small>
|
|
</div>
|
|
<span className={`badge ${liveData.manifest && liveData.status ? "ok" : "warn"}`}>
|
|
{liveData.manifest && liveData.status ? "Live" : "Unavailable"}
|
|
</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow
|
|
label="Manifest version"
|
|
value={String(liveData.manifest?.manifestVersion ?? "Unavailable")}
|
|
/>
|
|
<StatusRow
|
|
label="Storage provider"
|
|
value={liveData.manifest?.storageProvider ?? "Unavailable"}
|
|
/>
|
|
<StatusRow
|
|
label="Object prefix"
|
|
value={liveData.manifest?.objectKeyPrefix ?? "Unavailable"}
|
|
/>
|
|
<StatusRow
|
|
label="Parser policy"
|
|
value={liveData.manifest?.parserPolicyVersion ?? "Unavailable"}
|
|
/>
|
|
<StatusRow
|
|
label="Projection set"
|
|
value={liveData.manifest?.projectionSetVersion ?? "Unavailable"}
|
|
/>
|
|
<StatusRow
|
|
label="Raw bytes"
|
|
value={formatStatusNumber(liveData.status?.storage, "rawDocumentBytes")}
|
|
/>
|
|
<StatusRow
|
|
label="Documents"
|
|
value={formatStatusNumber(liveData.status?.storage, "documentCount")}
|
|
/>
|
|
<StatusRow
|
|
label="Active sessions"
|
|
value={formatStatusNumber(liveData.status?.runtime, "activeSessionCount")}
|
|
/>
|
|
</div>
|
|
</article>
|
|
|
|
<article id="operations-diagnostics" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Operations diagnostics</h2>
|
|
<small>Failed commits and active leases</small>
|
|
</div>
|
|
<span
|
|
className={`badge ${liveData.failedCommits && liveData.activeLeases ? "ok" : "warn"}`}
|
|
>
|
|
{liveData.failedCommits && liveData.activeLeases ? "Live" : "Unavailable"}
|
|
</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow
|
|
label="Failed commits"
|
|
value={String(liveData.failedCommits?.items.length ?? "Unavailable")}
|
|
/>
|
|
<StatusRow
|
|
label="Active leases"
|
|
value={String(liveData.activeLeases?.items.length ?? "Unavailable")}
|
|
/>
|
|
<StatusRow
|
|
label="Commit cursor"
|
|
value={liveData.failedCommits?.nextCursor ? "More available" : "Current page"}
|
|
/>
|
|
<StatusRow
|
|
label="Lease cursor"
|
|
value={liveData.activeLeases?.nextCursor ? "More available" : "Current page"}
|
|
/>
|
|
</div>
|
|
<div className="list-panel">
|
|
{(liveData.failedCommits?.items ?? []).slice(0, 5).map((commit, index) => (
|
|
<div className="list-row" key={adminRowKey("failed-commit", commit.id, index)}>
|
|
<strong>{commit.status}</strong>
|
|
<span>{commit.errorCode ?? commit.operationType}</span>
|
|
<code>{shortId(commit.id)}</code>
|
|
</div>
|
|
))}
|
|
{(liveData.activeLeases?.items ?? []).slice(0, 5).map((lease, index) => (
|
|
<div className="list-row" key={adminRowKey("active-lease", lease.id, index)}>
|
|
<strong>{lease.leaseType}</strong>
|
|
<span>{lease.virtualPath}</span>
|
|
<code>{shortId(lease.id)}</code>
|
|
</div>
|
|
))}
|
|
{(liveData.failedCommits?.items.length ?? 0) === 0 &&
|
|
(liveData.activeLeases?.items.length ?? 0) === 0 ? (
|
|
<p className="empty-state">No active diagnostics</p>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
|
|
<article id="fsck" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>FSCK dry run</h2>
|
|
<small>Raw object consistency check</small>
|
|
</div>
|
|
<span className={`badge ${liveData.fsck ? "ok" : "warn"}`}>
|
|
{liveData.fsck ? "Read only" : "Unavailable"}
|
|
</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow label="Scanned" value={formatFsckSummary(liveData.fsck, "scanned")} />
|
|
<StatusRow label="Errors" value={formatFsckSummary(liveData.fsck, "error")} />
|
|
<StatusRow label="Warnings" value={formatFsckSummary(liveData.fsck, "warning")} />
|
|
<StatusRow
|
|
label="Repairable"
|
|
value={formatFsckSummary(liveData.fsck, "repairable")}
|
|
/>
|
|
<StatusRow
|
|
label="Cursor"
|
|
value={liveData.fsck?.cursor ? "More available" : "Current page"}
|
|
/>
|
|
</div>
|
|
<div className="list-panel">
|
|
{(liveData.fsck?.issues ?? []).slice(0, 5).map((issue, index) => (
|
|
<div className="list-row" key={fsckIssueKey(issue, index)}>
|
|
<strong>{issue.severity}</strong>
|
|
<span>{issue.message}</span>
|
|
<code>{issue.type}</code>
|
|
</div>
|
|
))}
|
|
{(liveData.fsck?.issues.length ?? 0) === 0 ? (
|
|
<p className="empty-state">No fsck issues</p>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
|
|
<article id="gc-staged-objects" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Staged object GC</h2>
|
|
<small>Dry-run candidates</small>
|
|
</div>
|
|
<span className={`badge ${liveData.gcDryRun ? "warn" : "fail"}`}>
|
|
{liveData.gcDryRun ? "Dry run" : "Unavailable"}
|
|
</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow
|
|
label="Candidates"
|
|
value={formatGcSummary(liveData.gcDryRun, "candidateCount")}
|
|
/>
|
|
<StatusRow
|
|
label="Estimated bytes"
|
|
value={formatGcSummary(liveData.gcDryRun, "estimatedBytes")}
|
|
/>
|
|
<StatusRow
|
|
label="Dry-run id"
|
|
value={liveData.gcDryRun?.dryRunId ?? "Unavailable"}
|
|
mono
|
|
/>
|
|
<StatusRow
|
|
label="Cursor"
|
|
value={liveData.gcDryRun?.cursor ? "More available" : "Current page"}
|
|
/>
|
|
</div>
|
|
<div className="list-panel">
|
|
{(liveData.gcDryRun?.candidates ?? []).slice(0, 3).map((candidate) => (
|
|
<form
|
|
action="/api/admin-gc-staged-object"
|
|
className="list-row"
|
|
key={candidate.idempotencyKey}
|
|
method="post"
|
|
>
|
|
<input type="hidden" name="knowledgeSpaceId" value={activeWorkspaceId} />
|
|
<input type="hidden" name="dryRunId" value={liveData.gcDryRun?.dryRunId ?? ""} />
|
|
<input type="hidden" name="idempotencyKey" value={candidate.idempotencyKey} />
|
|
<input type="hidden" name="candidateJson" value={JSON.stringify(candidate)} />
|
|
<strong>{candidate.candidateType}</strong>
|
|
<span>{candidate.reason}</span>
|
|
<code>{shortId(candidate.idempotencyKey)}</code>
|
|
<button className="button secondary" type="submit">
|
|
Delete candidate
|
|
</button>
|
|
</form>
|
|
))}
|
|
{(liveData.gcDryRun?.candidates.length ?? 0) === 0 ? (
|
|
<p className="empty-state">No GC candidates</p>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
|
|
<article id="upload" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Upload intake</h2>
|
|
<small>Single document ingestion</small>
|
|
</div>
|
|
<span className="badge ok">Ready</span>
|
|
</div>
|
|
<form
|
|
action="/api/admin-upload"
|
|
className="form-grid"
|
|
encType="multipart/form-data"
|
|
method="post"
|
|
>
|
|
<div className="field">
|
|
<label htmlFor="space">Knowledge space</label>
|
|
<select id="space" name="knowledgeSpaceId" defaultValue={activeWorkspaceId}>
|
|
{resolvedWorkspaceOptions.map((workspace, index) => (
|
|
<option
|
|
key={adminRowKey("workspace-option", workspace.id, index)}
|
|
value={workspace.id}
|
|
>
|
|
{workspace.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="source">Source id</label>
|
|
<input id="source" name="sourceId" />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="file">Document</label>
|
|
<input
|
|
id="file"
|
|
name="file"
|
|
type="file"
|
|
accept=".md,.html,.pdf,.docx,.pptx,text/*"
|
|
/>
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button" type="submit">
|
|
Upload document
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<div className="button-row">
|
|
<a className="button secondary" href="/api/bff/health">
|
|
Check parser
|
|
</a>
|
|
</div>
|
|
{uploadResult ? <UploadResultCard result={uploadResult} /> : null}
|
|
</article>
|
|
|
|
<article className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Publish readiness</h2>
|
|
<small>{publishReadiness.subtitle}</small>
|
|
</div>
|
|
<span className={`badge ${publishReadiness.status}`}>{publishReadiness.badge}</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow label="Parser status" value={publishReadiness.parserStatus} />
|
|
<StatusRow label="Node count" value={publishReadiness.nodeCount} />
|
|
<StatusRow label="Quality risks" value={publishReadiness.qualityRisks} />
|
|
<StatusRow label="Parse elements" value={publishReadiness.elementCount} />
|
|
<StatusRow label="File size" value={publishReadiness.fileSize} />
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section className="grid work-grid" aria-label="Retrieval, entities, and traces">
|
|
<article id="retrieval" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Retrieval workspace</h2>
|
|
<small>{retrievalResult.subtitle}</small>
|
|
</div>
|
|
<span className="badge ok">Hybrid</span>
|
|
</div>
|
|
<form className="form-grid" action="/api/admin-query" method="post">
|
|
<input type="hidden" value={activeWorkspaceId} name="knowledgeSpaceId" />
|
|
<div className="field">
|
|
<label htmlFor="query">Query</label>
|
|
<input id="query" name="query" defaultValue={retrievalResult.query} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="mode">Mode</label>
|
|
<select id="mode" name="mode" defaultValue="fast">
|
|
<option value="fast">fast</option>
|
|
<option value="deep">deep</option>
|
|
<option value="research">research</option>
|
|
</select>
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button" type="submit">
|
|
Run query
|
|
</button>
|
|
{retrievalResult.traceId ? (
|
|
<a
|
|
className="button secondary"
|
|
href={`/api/bff/queries/${encodeURIComponent(retrievalResult.traceId)}`}
|
|
>
|
|
Open citations
|
|
</a>
|
|
) : (
|
|
<button className="button secondary" type="button">
|
|
Open citations
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
{queryResult?.status === "error" ? (
|
|
<section className="result-card fail" aria-label="Query failed" aria-live="polite">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>Query failed</h3>
|
|
<small>{queryResult.error}</small>
|
|
</div>
|
|
<span className="badge fail">failed</span>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
<div className="answer-preview" aria-label="Streaming answer">
|
|
<h3>Streaming answer</h3>
|
|
<p>{retrievalResult.answer}</p>
|
|
<div className="status-list">
|
|
<StatusRow label="Inline citations" value={retrievalResult.citations} />
|
|
<StatusRow label="Confidence" value={retrievalResult.confidence} />
|
|
<StatusRow label="Freshness" value={retrievalResult.freshness} />
|
|
{retrievalResult.traceId ? (
|
|
<StatusRow label="Trace ID" value={retrievalResult.traceId} mono />
|
|
) : null}
|
|
<StatusRow label="Knowledge space" value={activeWorkspaceId} mono />
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<article id="knowledge-fs" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>KnowledgeFS</h2>
|
|
<small>Live virtual filesystem listing</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
<form className="form-grid" action="/" method="get">
|
|
<input type="hidden" name="spaceId" value={activeWorkspaceId} />
|
|
<div className="field">
|
|
<label htmlFor="fsCommand">Command</label>
|
|
<select id="fsCommand" name="fsCommand" defaultValue={liveData.fsCommand.command}>
|
|
{knowledgeFsCommands.map((command) => (
|
|
<option key={command} value={command}>
|
|
{command}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsPath">Path</label>
|
|
<input id="fsPath" name="fsPath" defaultValue={liveData.fsPath} />
|
|
</div>
|
|
<div className="field span-2">
|
|
<label htmlFor="fsWriteText">Write text</label>
|
|
<textarea
|
|
id="fsWriteText"
|
|
name="fsWriteText"
|
|
defaultValue={liveData.fsCommand.writeText}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsLimit">Limit</label>
|
|
<input id="fsLimit" name="fsLimit" defaultValue={liveData.fsCommand.limit} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsDepth">Depth</label>
|
|
<input id="fsDepth" name="fsDepth" defaultValue={liveData.fsCommand.depth} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsGrepQuery">Grep query</label>
|
|
<input
|
|
id="fsGrepQuery"
|
|
name="fsGrepQuery"
|
|
defaultValue={liveData.fsCommand.grepQuery}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsFindNameContains">Find name contains</label>
|
|
<input
|
|
id="fsFindNameContains"
|
|
name="fsFindNameContains"
|
|
defaultValue={liveData.fsCommand.findNameContains}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsFindResourceType">Find resource type</label>
|
|
<input
|
|
id="fsFindResourceType"
|
|
name="fsFindResourceType"
|
|
defaultValue={liveData.fsCommand.findResourceType}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsFindMetadataKey">Find metadata key</label>
|
|
<input
|
|
id="fsFindMetadataKey"
|
|
name="fsFindMetadataKey"
|
|
defaultValue={liveData.fsCommand.findMetadataKey}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsFindMetadataValue">Find metadata value</label>
|
|
<input
|
|
id="fsFindMetadataValue"
|
|
name="fsFindMetadataValue"
|
|
defaultValue={liveData.fsCommand.findMetadataValue}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="fsNodeId">Node ID</label>
|
|
<input id="fsNodeId" name="fsNodeId" defaultValue={liveData.fsCommand.nodeId} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="oldPath">Old path</label>
|
|
<input id="oldPath" name="oldPath" defaultValue={liveData.oldPath} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="newPath">New path</label>
|
|
<input id="newPath" name="newPath" defaultValue={liveData.newPath} />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button secondary" type="submit">
|
|
Run command
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
formAction="/api/admin-knowledge-fs-write"
|
|
formMethod="post"
|
|
type="submit"
|
|
>
|
|
Run write/append
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<KnowledgeFsCommandReference />
|
|
<KnowledgeFsCommandResult state={liveData.fsCommand} />
|
|
</article>
|
|
|
|
<article id="entities" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Entity browser</h2>
|
|
<small>Live graph traversal and linked KnowledgeFS documents</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
<form className="form-grid" action="/" method="get">
|
|
<input type="hidden" name="spaceId" value={activeWorkspaceId} />
|
|
<div className="field">
|
|
<label htmlFor="entityId">Entity ID</label>
|
|
<input id="entityId" name="entityId" defaultValue={liveData.entityId} />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button secondary" type="submit">
|
|
Traverse graph
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{liveData.graph ? (
|
|
<>
|
|
<div className="status-list">
|
|
<StatusRow
|
|
label="Graph traversal"
|
|
value={`depth ${liveData.graph.metrics.depthReached}, fanout ${liveData.graph.metrics.fanout}, max ${liveData.graph.metrics.maxNodes}`}
|
|
/>
|
|
<StatusRow
|
|
label="Relations"
|
|
value={`${liveData.graph.relations.length} explored`}
|
|
/>
|
|
</div>
|
|
<div className="entity-layout">
|
|
<div className="entity-list" aria-label="Entities">
|
|
{liveData.graph.entities.map((entity, index) => (
|
|
<div className="entity-node" key={graphEntityRowKey(entity, index)}>
|
|
<div>
|
|
<strong>{entity.name}</strong>
|
|
<span>
|
|
{entity.type} - depth {entity.depth}
|
|
</span>
|
|
</div>
|
|
<em>{formatPercent(entity.confidence)}</em>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="graph-strip" aria-label="Relations">
|
|
{liveData.graph.relations.map((relation, index) => (
|
|
<div className="relation-line" key={graphRelationRowKey(relation, index)}>
|
|
<span>{relation.subjectEntityId}</span>
|
|
<strong>{relation.type}</strong>
|
|
<span>{relation.objectEntityId}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<EmptyState label="Enter an entity ID to traverse the live graph" />
|
|
)}
|
|
</article>
|
|
|
|
<article id="semantic-views" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Semantic views</h2>
|
|
<small>{semanticViewStatus.description}</small>
|
|
</div>
|
|
<span className={`badge ${semanticViewStatus.badgeStatus}`}>
|
|
{semanticViewStatus.badge}
|
|
</span>
|
|
</div>
|
|
<div className="semantic-view-grid" aria-label="Semantic view counts">
|
|
<div className="semantic-card">
|
|
<span>Topic browser</span>
|
|
<strong>{liveData.topicView?.items.length ?? 0}</strong>
|
|
<em>/knowledge/by-topic</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Readable entities</span>
|
|
<strong>
|
|
{liveData.entityView
|
|
? liveData.entityView.items.filter(isReadableSemanticEntityEntry).length
|
|
: 0}
|
|
</strong>
|
|
<em>/knowledge/by-entity</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Communities</span>
|
|
<strong>{liveData.communityView?.items.length ?? 0}</strong>
|
|
<em>/knowledge/by-community</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Workspace</span>
|
|
<strong>{liveData.activeWorkspace.name}</strong>
|
|
<em>{activeWorkspaceId}</em>
|
|
</div>
|
|
</div>
|
|
<form action="/api/admin-semantic-views" className="button-row" method="post">
|
|
<input type="hidden" name="knowledgeSpaceId" value={activeWorkspaceId} />
|
|
<input type="hidden" name="limit" value="50" />
|
|
<button
|
|
className="button secondary"
|
|
name="action"
|
|
value="materialize-topic"
|
|
type="submit"
|
|
>
|
|
Materialize topic view
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
name="action"
|
|
value="extract-entities"
|
|
type="submit"
|
|
>
|
|
Extract entities
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
name="action"
|
|
value="materialize-communities"
|
|
type="submit"
|
|
>
|
|
Materialize communities
|
|
</button>
|
|
</form>
|
|
<h3>Communities</h3>
|
|
<SemanticCommunityTable
|
|
list={liveData.communityView}
|
|
emptyLabel="No communities yet; run entity extraction and community materialization first"
|
|
/>
|
|
<h3>Topics</h3>
|
|
<SemanticTopicTable
|
|
list={liveData.topicView}
|
|
emptyLabel="No topic entries yet; run topic-view materialization or inspect Documents"
|
|
/>
|
|
<h3>Entities</h3>
|
|
<SemanticEntityTable
|
|
list={liveData.entityView}
|
|
emptyLabel="No entity entries yet; run entity extraction and graph indexing first"
|
|
/>
|
|
</article>
|
|
|
|
<article id="document-diff" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Document diff</h2>
|
|
<small>Live KnowledgeFS text and semantic diff</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
<form className="form-grid" action="/" method="get">
|
|
<input type="hidden" name="spaceId" value={activeWorkspaceId} />
|
|
<div className="field">
|
|
<label htmlFor="diffOldPath">Old path</label>
|
|
<input id="diffOldPath" name="oldPath" defaultValue={liveData.oldPath} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="diffNewPath">New path</label>
|
|
<input id="diffNewPath" name="newPath" defaultValue={liveData.newPath} />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button secondary" type="submit">
|
|
Run diff
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{liveData.diff ? (
|
|
<DocumentDiffView diff={liveData.diff} />
|
|
) : (
|
|
<EmptyState label="Choose two KnowledgeFS paths to compare" />
|
|
)}
|
|
</article>
|
|
|
|
<article id="golden-questions" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Golden questions</h2>
|
|
<small>Live expected evidence and annotation queue</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
<form action="/api/admin-golden-question" className="form-grid" method="post">
|
|
<input type="hidden" name="action" value="create" />
|
|
<input type="hidden" name="knowledgeSpaceId" value={activeWorkspaceId} />
|
|
<div className="field">
|
|
<label htmlFor="golden-question">Question</label>
|
|
<textarea id="golden-question" name="question" rows={3} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="expected-evidence">Expected evidence</label>
|
|
<input id="expected-evidence" name="expectedEvidenceIds" />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="golden-tags">Tags</label>
|
|
<input id="golden-tags" name="tags" />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button" type="submit">
|
|
Create question
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<form action="/api/admin-golden-question" className="form-grid" method="post">
|
|
<input type="hidden" name="knowledgeSpaceId" value={activeWorkspaceId} />
|
|
<div className="field">
|
|
<label htmlFor="selected-question">Question ID</label>
|
|
<input id="selected-question" name="questionId" />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="selected-question-text">Question</label>
|
|
<textarea id="selected-question-text" name="question" rows={2} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="selected-question-evidence">Expected evidence</label>
|
|
<input id="selected-question-evidence" name="expectedEvidenceIds" />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="selected-question-tags">Tags</label>
|
|
<input id="selected-question-tags" name="tags" />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button secondary" name="action" value="update" type="submit">
|
|
Update selected
|
|
</button>
|
|
<button className="button secondary" name="action" value="delete" type="submit">
|
|
Delete selected
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<GoldenQuestionAnnotationForm
|
|
activeWorkspaceId={activeWorkspaceId}
|
|
firstQuestionId={liveData.goldenQuestions?.items[0]?.id}
|
|
/>
|
|
<GoldenQuestionTable list={liveData.goldenQuestions} />
|
|
</article>
|
|
|
|
<article id="evaluation-dashboard" className="panel wide-panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Evaluation dashboard</h2>
|
|
<small>Live queue summary from golden questions</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
<div className="semantic-view-grid" aria-label="Evaluation scorecards">
|
|
<div className="semantic-card">
|
|
<span>Total questions</span>
|
|
<strong>{evaluationStats.total}</strong>
|
|
<em>golden set</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Annotated</span>
|
|
<strong>{evaluationStats.annotated}</strong>
|
|
<em>human review</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Bad cases</span>
|
|
<strong>{evaluationStats.badCases}</strong>
|
|
<em>production capture</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Pending</span>
|
|
<strong>{evaluationStats.pending}</strong>
|
|
<em>queue</em>
|
|
</div>
|
|
</div>
|
|
<form
|
|
id="bad-case-capture"
|
|
className="form-grid"
|
|
action="/api/admin-bad-case"
|
|
method="post"
|
|
>
|
|
<input type="hidden" name="knowledgeSpaceId" value={activeWorkspaceId} />
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>Production bad-case capture</h3>
|
|
<small>Trace and evidence context</small>
|
|
</div>
|
|
<span className="badge review">Eval queue</span>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="bad-case-trace">Trace ID</label>
|
|
<input
|
|
id="bad-case-trace"
|
|
name="traceId"
|
|
defaultValue={retrievalResult.traceId ?? ""}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="bad-case-reason">Reason</label>
|
|
<textarea id="bad-case-reason" name="reason" rows={3} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="bad-case-tags">Tags</label>
|
|
<input id="bad-case-tags" name="tags" />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button" type="submit">
|
|
Add to eval queue
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</article>
|
|
|
|
<article id="retrieval-studio" className="panel wide-panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Retrieval Studio</h2>
|
|
<small>Live evidence from the latest query trace</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
{liveData.trace ? (
|
|
<>
|
|
<div className="status-list">
|
|
<StatusRow label="Trace" value={liveData.trace.id} mono />
|
|
<StatusRow label="Mode" value={liveData.trace.mode} />
|
|
<StatusRow label="Query" value={liveData.trace.query} />
|
|
</div>
|
|
<KnowledgeFsTable
|
|
list={liveData.queryEvidence}
|
|
emptyLabel="No evidence entries recorded for this trace"
|
|
/>
|
|
</>
|
|
) : (
|
|
<EmptyState label="Run a query to inspect live retrieval evidence" />
|
|
)}
|
|
</article>
|
|
|
|
<article id="trace" className="panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Trace review</h2>
|
|
<small>Live recall, rerank, and evidence path</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
{liveData.trace ? (
|
|
<TraceReview trace={liveData.trace} />
|
|
) : (
|
|
<EmptyState label="Run a query to load a trace" />
|
|
)}
|
|
</article>
|
|
|
|
<article id="trace-comparison" className="panel wide-panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Trace comparison</h2>
|
|
<small>Live side-by-side trace comparison</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
<form className="form-grid" action="/" method="get">
|
|
<input type="hidden" name="spaceId" value={activeWorkspaceId} />
|
|
{retrievalResult.traceId ? (
|
|
<input type="hidden" name="traceId" value={retrievalResult.traceId} />
|
|
) : null}
|
|
<div className="field">
|
|
<label htmlFor="compareTraceId">Compare trace ID</label>
|
|
<input id="compareTraceId" name="compareTraceId" />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button secondary" type="submit">
|
|
Compare traces
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{liveData.trace && liveData.compareTrace ? (
|
|
<TraceComparisonView left={liveData.trace} right={liveData.compareTrace} />
|
|
) : (
|
|
<EmptyState label="Provide a second trace ID to compare with the latest query" />
|
|
)}
|
|
</article>
|
|
|
|
<article id="failed-query-diagnostics" className="panel wide-panel">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h2>Failed query diagnostics</h2>
|
|
<small>Live evidence, missing evidence, and conflict entries</small>
|
|
</div>
|
|
<span className="badge ok">Live</span>
|
|
</div>
|
|
{liveData.trace ? (
|
|
<div className="trend-layout">
|
|
<section aria-label="Candidate ranking">
|
|
<h3>Candidate ranking</h3>
|
|
<KnowledgeFsTable
|
|
list={liveData.queryEvidence}
|
|
emptyLabel="No ranked evidence entries"
|
|
/>
|
|
</section>
|
|
<section aria-label="Filter exclusions">
|
|
<h3>Filter exclusions</h3>
|
|
<KnowledgeFsTable
|
|
list={liveData.queryMissing ?? liveData.queryConflicts}
|
|
emptyLabel="No missing or conflicting evidence entries"
|
|
/>
|
|
</section>
|
|
</div>
|
|
) : (
|
|
<EmptyState label="Run a query to inspect live diagnostics" />
|
|
)}
|
|
</article>
|
|
</section>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function StatusRow({
|
|
label,
|
|
mono = false,
|
|
value,
|
|
}: {
|
|
readonly label: string;
|
|
readonly mono?: boolean;
|
|
readonly value: string;
|
|
}) {
|
|
return (
|
|
<div className="status-row">
|
|
<strong>{label}</strong>
|
|
<span className={mono ? "mono" : undefined}>{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyState({ label }: { readonly label: string }) {
|
|
return (
|
|
<div className="status-list">
|
|
<div className="status-row">
|
|
<strong>Status</strong>
|
|
<span>{label}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KnowledgeFsTable({
|
|
emptyLabel,
|
|
list,
|
|
}: {
|
|
readonly emptyLabel: string;
|
|
readonly list?: AdminKnowledgeFsList | undefined;
|
|
}) {
|
|
if (!list || list.items.length === 0) {
|
|
return <EmptyState label={emptyLabel} />;
|
|
}
|
|
|
|
return (
|
|
<div className="table" aria-label={`KnowledgeFS ${list.path}`}>
|
|
<div className="table-row">
|
|
<strong>{list.path}</strong>
|
|
<span>{list.truncated ? "truncated" : `${list.items.length} entries`}</span>
|
|
</div>
|
|
{list.items.map((entry) => (
|
|
<KnowledgeFsRow entry={entry} key={`${entry.kind}:${entry.path}`} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KnowledgeFsRow({ entry }: { readonly entry: AdminKnowledgeFsEntry }) {
|
|
return (
|
|
<div className="table-row">
|
|
<div>
|
|
<strong>{entry.name}</strong>
|
|
<div className="mono">{entry.path}</div>
|
|
<div className="mono">{entry.resourceType ?? entry.kind}</div>
|
|
</div>
|
|
<span>{entry.version ? `v${entry.version}` : (entry.targetId ?? entry.kind)}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KnowledgeFsCommandReference() {
|
|
return (
|
|
<div className="status-list" aria-label="KnowledgeFS commands">
|
|
<StatusRow
|
|
label="Commands"
|
|
value="ls, tree, cat, stat, grep, find, diff, open_node, write, append"
|
|
/>
|
|
<StatusRow label="Path commands" value="ls/tree/cat/stat/grep/find use Path" />
|
|
<StatusRow label="Diff" value="diff uses Old path and New path" />
|
|
<StatusRow label="Open node" value="open_node uses Node ID" />
|
|
<StatusRow label="Write" value="write overwrites Path with Write text" />
|
|
<StatusRow label="Append" value="append adds Write text to Path" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KnowledgeFsCommandResult({ state }: { readonly state: AdminKnowledgeFsCommandState }) {
|
|
if (state.error) {
|
|
return <EmptyState label={state.error} />;
|
|
}
|
|
|
|
if (!state.result) {
|
|
if (state.command === "write" || state.command === "append") {
|
|
return <EmptyState label="Use Run write/append to submit this write command" />;
|
|
}
|
|
|
|
return (
|
|
<EmptyState label={`Select ${state.command} parameters and run a KnowledgeFS command`} />
|
|
);
|
|
}
|
|
|
|
if (state.command === "cat") {
|
|
const result = state.result as AdminKnowledgeFsCat;
|
|
return (
|
|
<div className="table" aria-label="KnowledgeFS cat result">
|
|
<StatusRow label="cat" value={result.path} mono />
|
|
<StatusRow label="Content type" value={result.contentType} />
|
|
<StatusRow label="Length" value={`${result.text.length} chars`} />
|
|
<div className="table-row">
|
|
<strong>Text</strong>
|
|
<pre>{result.text.slice(0, 4000)}</pre>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (state.command === "stat") {
|
|
const result = state.result as AdminKnowledgeFsStat;
|
|
return (
|
|
<div className="status-list" aria-label="KnowledgeFS stat result">
|
|
<StatusRow label="Path" value={result.path} mono />
|
|
<StatusRow label="Resource" value={result.resourceType} />
|
|
<StatusRow label="Target ID" value={result.targetId} mono />
|
|
<StatusRow label="Version" value={result.version ? `v${result.version}` : "none"} />
|
|
<StatusRow label="Content type" value={result.contentType ?? "none"} />
|
|
<StatusRow label="Parser status" value={result.parserStatus ?? "none"} />
|
|
<StatusRow label="Size" value={result.sizeBytes ? formatBytes(result.sizeBytes) : "none"} />
|
|
<StatusRow label="SHA-256" value={result.sha256 ? shortId(result.sha256) : "none"} mono />
|
|
<StatusRow label="Metadata" value={formatJson(result.metadata)} mono />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (state.command === "grep") {
|
|
const result = state.result as AdminKnowledgeFsGrep;
|
|
if (result.matches.length === 0) {
|
|
return <EmptyState label="grep returned no matches" />;
|
|
}
|
|
|
|
return (
|
|
<div className="table" aria-label="KnowledgeFS grep result">
|
|
<div className="table-row">
|
|
<strong>{result.path}</strong>
|
|
<span>{result.truncated ? "truncated" : `${result.matches.length} matches`}</span>
|
|
</div>
|
|
{result.matches.map((match, index) => (
|
|
<div className="table-row" key={`${match.path}:${match.startOffset}:${index}`}>
|
|
<div>
|
|
<strong>{match.kind}</strong>
|
|
<div>{match.snippet}</div>
|
|
<div className="mono">{match.path}</div>
|
|
</div>
|
|
<span>
|
|
{match.startOffset}-{match.endOffset}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (state.command === "find") {
|
|
return (
|
|
<KnowledgeFsTable
|
|
list={state.result as AdminKnowledgeFsList}
|
|
emptyLabel="find returned no entries"
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (state.command === "tree") {
|
|
return <KnowledgeFsTreeView tree={state.result as AdminKnowledgeFsTree} />;
|
|
}
|
|
|
|
if (state.command === "diff") {
|
|
return <DocumentDiffView diff={state.result as AdminKnowledgeFsDiff} />;
|
|
}
|
|
|
|
if (state.command === "open_node") {
|
|
const result = state.result as AdminKnowledgeFsOpenNode;
|
|
return (
|
|
<div className="status-list" aria-label="KnowledgeFS open_node result">
|
|
<StatusRow label="Node ID" value={result.node.id} mono />
|
|
<StatusRow label="Kind" value={result.node.kind} />
|
|
<StatusRow label="Document" value={result.citation.documentAssetId} mono />
|
|
<StatusRow label="Parse artifact" value={result.citation.parseArtifactId} mono />
|
|
<StatusRow
|
|
label="Offsets"
|
|
value={`${result.citation.startOffset}-${result.citation.endOffset}`}
|
|
/>
|
|
<StatusRow label="Text" value={result.node.text.slice(0, 800)} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (state.command === "write" || state.command === "append") {
|
|
const result = state.result as AdminKnowledgeFsWrite;
|
|
|
|
return (
|
|
<div className="status-list" aria-label="KnowledgeFS write result">
|
|
<StatusRow label="Mode" value={result.mode} />
|
|
<StatusRow label="Path" value={result.path} mono />
|
|
<StatusRow label="Bytes written" value={formatBytes(result.bytesWritten)} />
|
|
<StatusRow label="Target ID" value={result.targetId} mono />
|
|
<StatusRow label="Version" value={`v${result.version}`} />
|
|
<StatusRow label="Object key" value={result.objectKey} mono />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<KnowledgeFsTable
|
|
list={state.result as AdminKnowledgeFsList}
|
|
emptyLabel="ls returned no entries"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function KnowledgeFsTreeView({ tree }: { readonly tree: AdminKnowledgeFsTree }) {
|
|
return (
|
|
<div className="table" aria-label="KnowledgeFS tree result">
|
|
<div className="table-row">
|
|
<strong>{tree.path}</strong>
|
|
<span>{tree.truncated ? "truncated" : "tree"}</span>
|
|
</div>
|
|
<KnowledgeFsTreeNodeRow node={tree.root} depth={0} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KnowledgeFsTreeNodeRow({
|
|
depth,
|
|
node,
|
|
}: {
|
|
readonly depth: number;
|
|
readonly node: AdminKnowledgeFsTreeNode;
|
|
}) {
|
|
return (
|
|
<>
|
|
<div className="table-row">
|
|
<div>
|
|
<strong>
|
|
{" ".repeat(depth)}
|
|
{node.name}
|
|
</strong>
|
|
<div className="mono">{node.path}</div>
|
|
</div>
|
|
<span>{node.resourceType ?? node.kind}</span>
|
|
</div>
|
|
{(node.children ?? []).map((child) => (
|
|
<KnowledgeFsTreeNodeRow
|
|
depth={depth + 1}
|
|
key={`${child.kind}:${child.path}`}
|
|
node={child}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SemanticTopicTable({
|
|
emptyLabel,
|
|
list,
|
|
}: {
|
|
readonly emptyLabel: string;
|
|
readonly list?: AdminKnowledgeFsList | undefined;
|
|
}) {
|
|
if (!list || list.items.length === 0) {
|
|
return <EmptyState label={emptyLabel} />;
|
|
}
|
|
|
|
return (
|
|
<div className="table" aria-label={`Semantic topics ${list.path}`}>
|
|
<div className="table-row">
|
|
<strong>Topic groups</strong>
|
|
<span>{formatEntryCount(list.items.length, "topic")}</span>
|
|
</div>
|
|
{list.items.map((entry) => {
|
|
const topicSlug = metadataString(entry.metadata, "topicSlug") ?? entry.name;
|
|
const topicName = metadataString(entry.metadata, "topicName") ?? titleizeSlug(topicSlug);
|
|
|
|
return (
|
|
<div className="table-row" key={`${entry.kind}:${entry.path}`}>
|
|
<div>
|
|
<strong>{topicName}</strong>
|
|
<div>{formatTopicKind(entry.kind)}</div>
|
|
<div className="mono">{entry.path}</div>
|
|
</div>
|
|
<span>{topicSlug}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SemanticCommunityTable({
|
|
emptyLabel,
|
|
list,
|
|
}: {
|
|
readonly emptyLabel: string;
|
|
readonly list?: AdminKnowledgeFsList | undefined;
|
|
}) {
|
|
if (!list || list.items.length === 0) {
|
|
return <EmptyState label={emptyLabel} />;
|
|
}
|
|
|
|
return (
|
|
<div className="table" aria-label={`Semantic communities ${list.path}`}>
|
|
<div className="table-row">
|
|
<strong>Knowledge communities</strong>
|
|
<span>{formatEntryCount(list.items.length, "community")}</span>
|
|
</div>
|
|
{list.items.map((entry) => {
|
|
const title = metadataString(entry.metadata, "title") ?? titleizeSlug(entry.name);
|
|
const summary = metadataString(entry.metadata, "summary") ?? "Summary pending";
|
|
const entityCount = metadataNumber(entry.metadata, "entityCount");
|
|
const documentCount = metadataNumber(entry.metadata, "documentCount");
|
|
|
|
return (
|
|
<div className="table-row" key={`${entry.kind}:${entry.path}`}>
|
|
<div>
|
|
<strong>{title}</strong>
|
|
<div>{summary}</div>
|
|
<div className="mono">{entry.path}</div>
|
|
</div>
|
|
<span>
|
|
{formatOptionalCounts([
|
|
entityCount === undefined ? undefined : formatEntryCount(entityCount, "entity"),
|
|
documentCount === undefined
|
|
? undefined
|
|
: formatEntryCount(documentCount, "document"),
|
|
])}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SemanticEntityTable({
|
|
emptyLabel,
|
|
list,
|
|
}: {
|
|
readonly emptyLabel: string;
|
|
readonly list?: AdminKnowledgeFsList | undefined;
|
|
}) {
|
|
if (!list || list.items.length === 0) {
|
|
return <EmptyState label={emptyLabel} />;
|
|
}
|
|
|
|
const visibleEntries = list.items.filter(isReadableSemanticEntityEntry);
|
|
if (visibleEntries.length === 0) {
|
|
return (
|
|
<div className="table" aria-label={`Semantic entities ${list.path}`}>
|
|
<div className="table-row">
|
|
<strong>Readable entities</strong>
|
|
<span>0 entries</span>
|
|
</div>
|
|
<div className="table-row">
|
|
<div>
|
|
<strong>No readable entities yet</strong>
|
|
<div>
|
|
Only low-signal numeric entries were found; run extraction after richer text is
|
|
uploaded
|
|
</div>
|
|
</div>
|
|
<span>{formatEntryCount(list.items.length, "hidden")}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const hiddenCount = list.items.length - visibleEntries.length;
|
|
|
|
return (
|
|
<div className="table" aria-label={`Semantic entities ${list.path}`}>
|
|
<div className="table-row">
|
|
<strong>Readable entities</strong>
|
|
<span>{formatEntryCount(visibleEntries.length, "entity")}</span>
|
|
</div>
|
|
{visibleEntries.map((entry) => {
|
|
const entityType = metadataString(entry.metadata, "type") ?? "entity";
|
|
const sourceNodeCount = metadataNumber(entry.metadata, "sourceNodeCount");
|
|
const entityId = metadataString(entry.metadata, "entityId") ?? entry.targetId;
|
|
|
|
return (
|
|
<div className="table-row" key={`${entry.kind}:${entry.path}`}>
|
|
<div>
|
|
<strong>{entry.name}</strong>
|
|
<div>
|
|
{formatEntityType(entityType)}
|
|
{sourceNodeCount === undefined
|
|
? ""
|
|
: ` - seen in ${formatEntryCount(sourceNodeCount, "node")}`}
|
|
</div>
|
|
{entityId ? <div className="mono">Graph ID {shortId(entityId)}</div> : null}
|
|
</div>
|
|
<span>{entry.kind}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
{hiddenCount > 0 ? (
|
|
<div className="table-row">
|
|
<div>
|
|
<strong>Hidden numeric noise</strong>
|
|
<div>Plain numbers are suppressed from this operator view</div>
|
|
</div>
|
|
<span>{formatEntryCount(hiddenCount, "entry")}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DocumentDiffView({ diff }: { readonly diff: AdminKnowledgeFsDiff }) {
|
|
return (
|
|
<>
|
|
<div className="status-list">
|
|
<StatusRow label="Old path" value={diff.oldPath} mono />
|
|
<StatusRow label="New path" value={diff.newPath} mono />
|
|
<StatusRow
|
|
label="Stats"
|
|
value={`+${diff.stats.insert} / -${diff.stats.delete} / = ${diff.stats.equal}`}
|
|
/>
|
|
</div>
|
|
<div className="diff-layout">
|
|
<div aria-label="Text diff">
|
|
<h3>Text diff</h3>
|
|
<div className="diff-lines">
|
|
{diff.operations.slice(0, 20).map((operation, index) => (
|
|
<div className={`diff-line ${operation.kind}`} key={`${operation.kind}-${index}`}>
|
|
<span>{operation.kind}</span>
|
|
<p>{operation.text}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div aria-label="Semantic summary">
|
|
<h3>Semantic summary</h3>
|
|
<p>{diff.semantic?.summary ?? "Semantic diff was not returned."}</p>
|
|
<div className="table">
|
|
{(diff.semantic?.changes ?? []).map((change, index) => (
|
|
<div
|
|
className="table-row"
|
|
key={adminRowKey("semantic-change", change.summary, index)}
|
|
>
|
|
<div>
|
|
<strong>{change.category}</strong>
|
|
<div className="mono">{change.evidence.join(", ")}</div>
|
|
</div>
|
|
<span>{change.summary}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function GoldenQuestionTable({ list }: { readonly list?: AdminGoldenQuestionList | undefined }) {
|
|
if (!list || list.items.length === 0) {
|
|
return <EmptyState label="No golden questions found" />;
|
|
}
|
|
|
|
return (
|
|
<div className="table" aria-label="Golden question list">
|
|
<div className="table-row">
|
|
<strong>Question</strong>
|
|
<span>Expected evidence</span>
|
|
</div>
|
|
{list.items.map((question, index) => (
|
|
<div className="table-row" key={adminRowKey("golden-question", question.id, index)}>
|
|
<div>
|
|
<strong>{question.question}</strong>
|
|
<div className="mono">{question.id}</div>
|
|
<div className="mono">{question.tags.join(", ") || "untagged"}</div>
|
|
</div>
|
|
<span>{question.expectedEvidenceIds.join(", ") || "None"}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GoldenQuestionAnnotationForm({
|
|
activeWorkspaceId,
|
|
firstQuestionId,
|
|
}: {
|
|
readonly activeWorkspaceId: string;
|
|
readonly firstQuestionId?: string | undefined;
|
|
}) {
|
|
return (
|
|
<form action="/api/admin-golden-annotation" className="form-grid" method="post">
|
|
<input type="hidden" name="knowledgeSpaceId" value={activeWorkspaceId} />
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>Human annotation</h3>
|
|
<small>Evidence relevance and answer correctness</small>
|
|
</div>
|
|
<span className="badge review">Review queue</span>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="annotation-question-id">Question ID</label>
|
|
<input id="annotation-question-id" name="questionId" defaultValue={firstQuestionId ?? ""} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="answer-correctness">Answer correctness</label>
|
|
<select id="answer-correctness" name="answerCorrectness" defaultValue="incorrect">
|
|
<option value="correct">correct</option>
|
|
<option value="partially-correct">partially-correct</option>
|
|
<option value="incorrect">incorrect</option>
|
|
<option value="not-answerable">not-answerable</option>
|
|
</select>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="evidence-relevance">Evidence relevance</label>
|
|
<textarea id="evidence-relevance" name="evidenceRelevance" rows={3} />
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="annotation-note">Annotation note</label>
|
|
<textarea id="annotation-note" name="note" rows={3} />
|
|
</div>
|
|
<div className="button-row">
|
|
<button className="button" type="submit">
|
|
Submit annotation
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function TraceReview({ trace }: { readonly trace: AdminAnswerTrace }) {
|
|
const summary = createTraceSummary({
|
|
steps: trace.steps.map((step) => ({ attributes: step.metadata, name: step.name })),
|
|
});
|
|
const treeSearch = extractReasoningTreeSearchTrace(trace);
|
|
|
|
return (
|
|
<>
|
|
<div className="table">
|
|
<StatusRow label="Route" value={summary.route} />
|
|
<StatusRow label="Recall candidates" value={String(summary.recallCandidates)} />
|
|
<StatusRow label="Filters" value={summary.filters} />
|
|
<StatusRow label="Rerank" value={summary.rerank} />
|
|
<StatusRow label="Evidence" value={summary.evidence} />
|
|
</div>
|
|
{treeSearch ? (
|
|
<div className="table" aria-label="Reasoning tree search trace">
|
|
<StatusRow label="Tree search" value={treeSearch.strategy} />
|
|
<StatusRow label="Selected section" value={treeSearch.selectedSectionPath} />
|
|
<StatusRow label="Selected node" value={treeSearch.selectedNodeId} mono />
|
|
<StatusRow label="Inspected nodes" value={treeSearch.inspectedNodeIds} mono />
|
|
<StatusRow label="Opened ranges" value={treeSearch.openedRanges} />
|
|
<StatusRow label="Final evidence" value={treeSearch.finalEvidenceNodeIds} mono />
|
|
<StatusRow label="Reasoning" value={treeSearch.reasoning} />
|
|
</div>
|
|
) : null}
|
|
<div className="table">
|
|
{trace.steps.map((step, index) => (
|
|
<div
|
|
className="table-row"
|
|
key={adminRowKey("trace-step", `${step.name}:${step.startedAt}`, index)}
|
|
>
|
|
<div>
|
|
<strong>{step.name}</strong>
|
|
<div className="mono">{step.startedAt}</div>
|
|
</div>
|
|
<span>{step.status}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface ReasoningTreeSearchTraceView {
|
|
readonly finalEvidenceNodeIds: string;
|
|
readonly inspectedNodeIds: string;
|
|
readonly openedRanges: string;
|
|
readonly reasoning: string;
|
|
readonly selectedNodeId: string;
|
|
readonly selectedSectionPath: string;
|
|
readonly strategy: string;
|
|
}
|
|
|
|
function extractReasoningTreeSearchTrace(
|
|
trace: AdminAnswerTrace,
|
|
): ReasoningTreeSearchTraceView | undefined {
|
|
for (const step of trace.steps) {
|
|
const directTrace = traceViewFromRecord(recordFromUnknown(step.metadata.reasoningTreeSearch));
|
|
if (directTrace) {
|
|
return directTrace;
|
|
}
|
|
|
|
const evidenceBundle = recordFromUnknown(step.metadata.evidenceBundle);
|
|
const items = arrayFromRecord(evidenceBundle, "items");
|
|
const itemTrace = items
|
|
?.map((item) => {
|
|
const metadata = recordFromUnknown(recordFromUnknown(item)?.metadata);
|
|
return traceViewFromRecord(recordFromUnknown(metadata?.reasoningTreeSearch));
|
|
})
|
|
.find((traceView) => traceView !== undefined);
|
|
|
|
if (itemTrace) {
|
|
return itemTrace;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function traceViewFromRecord(
|
|
trace: Readonly<Record<string, unknown>> | undefined,
|
|
): ReasoningTreeSearchTraceView | undefined {
|
|
if (!trace) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
finalEvidenceNodeIds: formatStringArray(arrayFromRecord(trace, "finalEvidenceNodeIds")),
|
|
inspectedNodeIds: formatStringArray(arrayFromRecord(trace, "inspectedNodeIds")),
|
|
openedRanges: formatOpenedRanges(arrayFromRecord(trace, "openedRanges")),
|
|
reasoning: stringFromRecord(trace, "reasoning") ?? "No tree-search reasoning recorded.",
|
|
selectedNodeId: stringFromRecord(trace, "selectedNodeId") ?? "unknown",
|
|
selectedSectionPath: formatStringArray(arrayFromRecord(trace, "selectedSectionPath")),
|
|
strategy: stringFromRecord(trace, "strategy") ?? "unknown",
|
|
};
|
|
}
|
|
|
|
function formatOpenedRanges(values: readonly unknown[] | undefined): string {
|
|
if (!values || values.length === 0) {
|
|
return "none";
|
|
}
|
|
|
|
return values
|
|
.map((value) => {
|
|
const range = recordFromUnknown(value);
|
|
if (!range) {
|
|
return undefined;
|
|
}
|
|
|
|
const sectionPath = formatStringArray(arrayFromRecord(range, "sectionPath"));
|
|
const startPage = numberFromRecord(range, "startPage");
|
|
const endPage = numberFromRecord(range, "endPage");
|
|
const startOffset = numberFromRecord(range, "startOffset");
|
|
const endOffset = numberFromRecord(range, "endOffset");
|
|
const page =
|
|
startPage === undefined
|
|
? "page unknown"
|
|
: endPage !== undefined && endPage !== startPage
|
|
? `pages ${startPage}-${endPage}`
|
|
: `page ${startPage}`;
|
|
const offset =
|
|
startOffset === undefined
|
|
? ""
|
|
: endOffset !== undefined && endOffset !== startOffset
|
|
? ` offset ${startOffset}-${endOffset}`
|
|
: ` offset ${startOffset}`;
|
|
|
|
return `${sectionPath} (${page}${offset})`;
|
|
})
|
|
.filter((value): value is string => value !== undefined)
|
|
.join("; ");
|
|
}
|
|
|
|
function formatStringArray(values: readonly unknown[] | undefined): string {
|
|
const strings = values?.filter((value): value is string => typeof value === "string") ?? [];
|
|
return strings.length > 0 ? strings.join(" / ") : "none";
|
|
}
|
|
|
|
function recordFromUnknown(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Readonly<Record<string, unknown>>)
|
|
: undefined;
|
|
}
|
|
|
|
function arrayFromRecord(
|
|
value: Readonly<Record<string, unknown>> | undefined,
|
|
key: string,
|
|
): readonly unknown[] | undefined {
|
|
const candidate = value?.[key];
|
|
return Array.isArray(candidate) ? candidate : undefined;
|
|
}
|
|
|
|
function stringFromRecord(
|
|
value: Readonly<Record<string, unknown>>,
|
|
key: string,
|
|
): string | undefined {
|
|
const candidate = value[key];
|
|
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate : undefined;
|
|
}
|
|
|
|
function numberFromRecord(
|
|
value: Readonly<Record<string, unknown>>,
|
|
key: string,
|
|
): number | undefined {
|
|
const candidate = value[key];
|
|
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
|
|
}
|
|
|
|
function TraceComparisonView({
|
|
left,
|
|
right,
|
|
}: {
|
|
readonly left: AdminAnswerTrace;
|
|
readonly right: AdminAnswerTrace;
|
|
}) {
|
|
const comparison = createTraceComparison({
|
|
traces: [
|
|
{
|
|
id: left.id,
|
|
label: "baseline",
|
|
steps: left.steps.map((step) => ({ attributes: step.metadata, name: step.name })),
|
|
},
|
|
{
|
|
id: right.id,
|
|
label: "challenger",
|
|
steps: right.steps.map((step) => ({ attributes: step.metadata, name: step.name })),
|
|
},
|
|
],
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<div className="semantic-view-grid" aria-label="Trace comparison deltas">
|
|
<div className="semantic-card">
|
|
<span>Recall delta</span>
|
|
<strong>{comparison.deltas.recallDeltaLabel}</strong>
|
|
<em>{comparison.deltas.routeChangeLabel}</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Citation delta</span>
|
|
<strong>{comparison.deltas.citationDeltaLabel}</strong>
|
|
<em>{comparison.deltas.rerankChangeLabel}</em>
|
|
</div>
|
|
<div className="semantic-card">
|
|
<span>Filter changes</span>
|
|
<strong>{comparison.deltas.filterChangeLabel}</strong>
|
|
<em>bounded trace steps</em>
|
|
</div>
|
|
</div>
|
|
<div className="strategy-grid" aria-label="Trace columns">
|
|
{comparison.columns.map((column, index) => (
|
|
<section
|
|
className="strategy-column"
|
|
key={adminRowKey("trace-column", column.traceId, index)}
|
|
>
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>{column.label}</h3>
|
|
<small className="mono">{column.traceId}</small>
|
|
</div>
|
|
<span className="badge ok">{column.stepCountLabel}</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow label="Route" value={column.routeLabel} />
|
|
<StatusRow label="Recall" value={column.recallCandidatesLabel} />
|
|
<StatusRow label="Rerank" value={column.rerankLabel} />
|
|
<StatusRow label="Evidence" value={column.evidenceLabel} />
|
|
<StatusRow label="Filters" value={column.filtersLabel} />
|
|
</div>
|
|
</section>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function UploadResultCard({ result }: { readonly result: AdminUploadResult }) {
|
|
if (result.status === "error") {
|
|
return (
|
|
<section className="result-card fail" aria-label="Upload failed" aria-live="polite">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>Upload failed</h3>
|
|
<small>{result.error}</small>
|
|
</div>
|
|
<span className="badge fail">failed</span>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const documentHref = `/documents/${encodeURIComponent(result.spaceId)}/${encodeURIComponent(result.documentId)}`;
|
|
const artifactHref = `${documentHref}/parse-artifacts/${result.version}`;
|
|
|
|
return (
|
|
<section className="result-card" aria-label="Upload result" aria-live="polite">
|
|
<div className="panel-header">
|
|
<div>
|
|
<h3>Upload result</h3>
|
|
<small>{result.filename}</small>
|
|
</div>
|
|
<span className={`badge ${result.parserStatus === "failed" ? "fail" : "ok"}`}>
|
|
{result.parserStatus}
|
|
</span>
|
|
</div>
|
|
<div className="status-list">
|
|
<StatusRow label="Document id" value={result.documentId} mono />
|
|
<StatusRow label="Size" value={formatBytes(result.sizeBytes)} />
|
|
<StatusRow label="SHA-256" value={result.sha256.slice(0, 12)} mono />
|
|
</div>
|
|
<div className="button-row">
|
|
<a className="button secondary" href={documentHref}>
|
|
Open document status
|
|
</a>
|
|
<a className="button secondary" href={artifactHref}>
|
|
Open parse artifact
|
|
</a>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
async function loadAdminLiveData({
|
|
queryResult,
|
|
searchParams,
|
|
uploadResult,
|
|
workspaceOptions,
|
|
}: {
|
|
readonly queryResult: AdminQueryResult | null;
|
|
readonly searchParams: Record<string, string | string[] | undefined>;
|
|
readonly uploadResult: AdminUploadResult | null;
|
|
readonly workspaceOptions: readonly AdminWorkspaceOption[];
|
|
}): Promise<AdminLiveData> {
|
|
const activeWorkspace = selectActiveWorkspace(workspaceOptions, searchParams, uploadResult);
|
|
const token = getAdminServerToken();
|
|
const fsPath = firstSearchParam(searchParams.fsPath) || defaultKnowledgeFsPath;
|
|
const fsCommand = parseKnowledgeFsCommand(firstSearchParam(searchParams.fsCommand));
|
|
const fsLimit = firstSearchParam(searchParams.fsLimit) || "12";
|
|
const rootFsLimit = parseFormPositiveInteger(fsLimit, 12);
|
|
const fsDepth = firstSearchParam(searchParams.fsDepth) || "3";
|
|
const fsGrepQuery = firstSearchParam(searchParams.fsGrepQuery) || "";
|
|
const fsFindNameContains = firstSearchParam(searchParams.fsFindNameContains) || "";
|
|
const fsFindResourceType = firstSearchParam(searchParams.fsFindResourceType) || "";
|
|
const fsFindMetadataKey = firstSearchParam(searchParams.fsFindMetadataKey) || "";
|
|
const fsFindMetadataValue = firstSearchParam(searchParams.fsFindMetadataValue) || "";
|
|
const fsNodeId = firstSearchParam(searchParams.fsNodeId) || "";
|
|
const fsWriteText = firstSearchParam(searchParams.fsWriteText) || "";
|
|
const entityId = firstSearchParam(searchParams.entityId) || "";
|
|
const oldPath = firstSearchParam(searchParams.oldPath) || "";
|
|
const newPath = firstSearchParam(searchParams.newPath) || "";
|
|
const traceId =
|
|
firstSearchParam(searchParams.traceId) ||
|
|
(queryResult?.status === "success" ? queryResult.traceId : undefined) ||
|
|
"";
|
|
const traceLookupId = isUuid(traceId) ? traceId : "";
|
|
const compareTraceId = firstSearchParam(searchParams.compareTraceId) || "";
|
|
const compareTraceLookupId = isUuid(compareTraceId) ? compareTraceId : "";
|
|
|
|
if (!token) {
|
|
return {
|
|
activeWorkspace,
|
|
entityId,
|
|
errors: ["Admin API token is not configured"],
|
|
fsCommand: {
|
|
command: fsCommand,
|
|
depth: fsDepth,
|
|
findMetadataKey: fsFindMetadataKey,
|
|
findMetadataValue: fsFindMetadataValue,
|
|
findNameContains: fsFindNameContains,
|
|
findResourceType: fsFindResourceType,
|
|
grepQuery: fsGrepQuery,
|
|
limit: fsLimit,
|
|
nodeId: fsNodeId,
|
|
writeText: fsWriteText,
|
|
},
|
|
fsPath,
|
|
newPath,
|
|
oldPath,
|
|
};
|
|
}
|
|
|
|
const client = createAdminApiClient({ baseUrl: getAdminApiBase() });
|
|
const [
|
|
rootFs,
|
|
topicView,
|
|
entityView,
|
|
communityView,
|
|
goldenQuestions,
|
|
trace,
|
|
compareTrace,
|
|
queryEvidence,
|
|
queryMissing,
|
|
queryConflicts,
|
|
graph,
|
|
diff,
|
|
manifest,
|
|
status,
|
|
failedCommits,
|
|
activeLeases,
|
|
fsck,
|
|
gcDryRun,
|
|
publishReadiness,
|
|
fsCommandResult,
|
|
] = await Promise.all([
|
|
loadLive("KnowledgeFS", () =>
|
|
client.listKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: rootFsLimit,
|
|
path: fsPath,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("Topic view", () =>
|
|
client.listSemanticView({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: 8,
|
|
token,
|
|
view: "by-topic",
|
|
}),
|
|
),
|
|
loadLive("Entity view", () =>
|
|
client.listSemanticView({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: 8,
|
|
token,
|
|
view: "by-entity",
|
|
}),
|
|
),
|
|
loadLive("Community view", () =>
|
|
client.listSemanticView({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: 8,
|
|
token,
|
|
view: "by-community",
|
|
}),
|
|
),
|
|
loadLive("Golden questions", () =>
|
|
client.listGoldenQuestions({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: 12,
|
|
token,
|
|
}),
|
|
),
|
|
traceLookupId
|
|
? loadLive("Answer trace", () => client.getAnswerTrace({ token, traceId: traceLookupId }))
|
|
: emptyLive<AdminAnswerTrace>(),
|
|
compareTraceLookupId
|
|
? loadLive("Compare trace", () =>
|
|
client.getAnswerTrace({ token, traceId: compareTraceLookupId }),
|
|
)
|
|
: emptyLive<AdminAnswerTrace>(),
|
|
traceLookupId
|
|
? loadLive("Query evidence", () =>
|
|
client.listQueryEvidence({ limit: 12, token, traceId: traceLookupId }),
|
|
)
|
|
: emptyLive<AdminKnowledgeFsList>(),
|
|
traceLookupId
|
|
? loadLive("Query missing evidence", () =>
|
|
client.listQueryMissing({ limit: 12, token, traceId: traceLookupId }),
|
|
)
|
|
: emptyLive<AdminKnowledgeFsList>(),
|
|
traceLookupId
|
|
? loadLive("Query conflicts", () =>
|
|
client.listQueryConflicts({ limit: 12, token, traceId: traceLookupId }),
|
|
)
|
|
: emptyLive<AdminKnowledgeFsList>(),
|
|
entityId
|
|
? loadLive("Graph traversal", () =>
|
|
client.traverseGraph({
|
|
depth: 2,
|
|
entityId,
|
|
fanout: 8,
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
maxNodes: 30,
|
|
timeoutMs: 500,
|
|
token,
|
|
}),
|
|
)
|
|
: emptyLive<AdminGraphTraversal>(),
|
|
oldPath && newPath
|
|
? loadLive("Document diff", () =>
|
|
client.diffKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
mode: "word",
|
|
newPath,
|
|
oldPath,
|
|
semantic: true,
|
|
token,
|
|
}),
|
|
)
|
|
: emptyLive<AdminKnowledgeFsDiff>(),
|
|
loadLive("KnowledgeSpace manifest", () =>
|
|
client.getKnowledgeSpaceManifest({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("KnowledgeSpace status", () =>
|
|
client.getKnowledgeSpaceStatus({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("KnowledgeSpace failed commits", () =>
|
|
client.listStagedCommits({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: 5,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("KnowledgeSpace active leases", () =>
|
|
client.listActiveLeases({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
limit: 5,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("KnowledgeSpace fsck", () =>
|
|
client.getKnowledgeFsck({
|
|
check: "raw-objects",
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("KnowledgeSpace staged-object GC dry-run", () =>
|
|
client.getStagedObjectGcDryRun({
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
token,
|
|
}),
|
|
),
|
|
loadLive("Document readiness", () =>
|
|
loadDocumentPublishReadiness({
|
|
client,
|
|
knowledgeSpaceId: activeWorkspace.id,
|
|
searchParams,
|
|
token,
|
|
uploadResult,
|
|
}),
|
|
),
|
|
fsCommand === "ls" || fsCommand === "write" || fsCommand === "append"
|
|
? emptyLive<AdminKnowledgeFsCommandState["result"]>()
|
|
: loadLive("KnowledgeFS command", () =>
|
|
loadKnowledgeFsCommandResult({
|
|
activeWorkspaceId: activeWorkspace.id,
|
|
client,
|
|
command: fsCommand,
|
|
depth: fsDepth,
|
|
findMetadataKey: fsFindMetadataKey,
|
|
findMetadataValue: fsFindMetadataValue,
|
|
findNameContains: fsFindNameContains,
|
|
findResourceType: fsFindResourceType,
|
|
grepQuery: fsGrepQuery,
|
|
limit: fsLimit,
|
|
newPath,
|
|
nodeId: fsNodeId,
|
|
oldPath,
|
|
path: fsPath,
|
|
token,
|
|
}),
|
|
),
|
|
]);
|
|
|
|
return {
|
|
activeWorkspace,
|
|
...(activeLeases.value ? { activeLeases: activeLeases.value } : {}),
|
|
...(compareTrace.value ? { compareTrace: compareTrace.value } : {}),
|
|
...(communityView.value ? { communityView: communityView.value } : {}),
|
|
...(diff.value ? { diff: diff.value } : {}),
|
|
entityId,
|
|
...(entityView.value ? { entityView: entityView.value } : {}),
|
|
errors: [
|
|
manifest.error,
|
|
status.error,
|
|
failedCommits.error,
|
|
activeLeases.error,
|
|
fsck.error,
|
|
gcDryRun.error,
|
|
goldenQuestions.error,
|
|
visibleLiveDataError(trace.error),
|
|
visibleLiveDataError(compareTrace.error),
|
|
visibleLiveDataError(queryEvidence.error),
|
|
visibleLiveDataError(queryMissing.error),
|
|
visibleLiveDataError(queryConflicts.error),
|
|
].filter(Boolean) as string[],
|
|
...(failedCommits.value ? { failedCommits: failedCommits.value } : {}),
|
|
fsCommand: {
|
|
command: fsCommand,
|
|
depth: fsDepth,
|
|
...(fsCommand === "ls" && rootFs.error ? { error: rootFs.error } : {}),
|
|
...(fsCommand !== "ls" && fsCommandResult.error ? { error: fsCommandResult.error } : {}),
|
|
findMetadataKey: fsFindMetadataKey,
|
|
findMetadataValue: fsFindMetadataValue,
|
|
findNameContains: fsFindNameContains,
|
|
findResourceType: fsFindResourceType,
|
|
grepQuery: fsGrepQuery,
|
|
limit: fsLimit,
|
|
nodeId: fsNodeId,
|
|
result: fsCommand === "ls" ? rootFs.value : fsCommandResult.value,
|
|
writeText: fsWriteText,
|
|
},
|
|
...(fsck.value ? { fsck: fsck.value } : {}),
|
|
...(gcDryRun.value ? { gcDryRun: gcDryRun.value } : {}),
|
|
fsPath,
|
|
...(graph.value ? { graph: graph.value } : {}),
|
|
...(goldenQuestions.value ? { goldenQuestions: goldenQuestions.value } : {}),
|
|
...(manifest.value ? { manifest: manifest.value } : {}),
|
|
newPath,
|
|
oldPath,
|
|
...(publishReadiness.value ? { publishReadiness: publishReadiness.value } : {}),
|
|
...(queryConflicts.value ? { queryConflicts: queryConflicts.value } : {}),
|
|
...(queryEvidence.value ? { queryEvidence: queryEvidence.value } : {}),
|
|
...(queryMissing.value ? { queryMissing: queryMissing.value } : {}),
|
|
...(rootFs.value ? { rootFs: rootFs.value } : {}),
|
|
...(status.value ? { status: status.value } : {}),
|
|
...(topicView.value ? { topicView: topicView.value } : {}),
|
|
...(trace.value ? { trace: trace.value } : {}),
|
|
};
|
|
}
|
|
|
|
async function loadKnowledgeFsCommandResult({
|
|
activeWorkspaceId,
|
|
client,
|
|
command,
|
|
depth,
|
|
findMetadataKey,
|
|
findMetadataValue,
|
|
findNameContains,
|
|
findResourceType,
|
|
grepQuery,
|
|
limit,
|
|
newPath,
|
|
nodeId,
|
|
oldPath,
|
|
path,
|
|
token,
|
|
}: {
|
|
readonly activeWorkspaceId: string;
|
|
readonly client: AdminApiClient;
|
|
readonly command: AdminKnowledgeFsCommand;
|
|
readonly depth: string;
|
|
readonly findMetadataKey: string;
|
|
readonly findMetadataValue: string;
|
|
readonly findNameContains: string;
|
|
readonly findResourceType: string;
|
|
readonly grepQuery: string;
|
|
readonly limit: string;
|
|
readonly newPath: string;
|
|
readonly nodeId: string;
|
|
readonly oldPath: string;
|
|
readonly path: string;
|
|
readonly token: string;
|
|
}): Promise<AdminKnowledgeFsCommandState["result"]> {
|
|
const parsedLimit = parseFormPositiveInteger(limit, 12);
|
|
const parsedDepth = parseFormPositiveInteger(depth, 3);
|
|
|
|
if (command === "tree") {
|
|
return client.treeKnowledgeFs({
|
|
depth: parsedDepth,
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
limit: parsedLimit,
|
|
path,
|
|
token,
|
|
});
|
|
}
|
|
|
|
if (command === "cat") {
|
|
return client.catKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
limit: parsedLimit,
|
|
path,
|
|
token,
|
|
});
|
|
}
|
|
|
|
if (command === "stat") {
|
|
return client.statKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
path,
|
|
token,
|
|
});
|
|
}
|
|
|
|
if (command === "grep") {
|
|
return client.grepKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
limit: parsedLimit,
|
|
path,
|
|
q: grepQuery,
|
|
token,
|
|
});
|
|
}
|
|
|
|
if (command === "find") {
|
|
return client.findKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
limit: parsedLimit,
|
|
path,
|
|
...(findMetadataKey ? { metadataKey: findMetadataKey } : {}),
|
|
...(findMetadataValue ? { metadataValue: findMetadataValue } : {}),
|
|
...(findNameContains ? { nameContains: findNameContains } : {}),
|
|
...(findResourceType
|
|
? { resourceType: findResourceType as AdminKnowledgeFsEntry["resourceType"] }
|
|
: {}),
|
|
token,
|
|
});
|
|
}
|
|
|
|
if (command === "diff") {
|
|
return client.diffKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
mode: "word",
|
|
newPath,
|
|
oldPath,
|
|
semantic: true,
|
|
token,
|
|
});
|
|
}
|
|
|
|
if (command === "open_node") {
|
|
return client.openKnowledgeFsNode({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
nodeId,
|
|
token,
|
|
});
|
|
}
|
|
|
|
return client.listKnowledgeFs({
|
|
knowledgeSpaceId: activeWorkspaceId,
|
|
limit: parsedLimit,
|
|
path,
|
|
token,
|
|
});
|
|
}
|
|
|
|
function emptyLive<T>(): Promise<Loaded<T>> {
|
|
return Promise.resolve({});
|
|
}
|
|
|
|
function visibleLiveDataError(error: string | undefined): string | undefined {
|
|
if (!error?.includes("Answer trace not found")) {
|
|
return error;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
async function loadLive<T>(label: string, task: () => Promise<T>): Promise<Loaded<T>> {
|
|
try {
|
|
return { value: await task() };
|
|
} catch (error) {
|
|
return {
|
|
error: `${label}: ${error instanceof Error ? error.message : "failed"}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function loadDocumentPublishReadiness({
|
|
client,
|
|
knowledgeSpaceId,
|
|
searchParams,
|
|
token,
|
|
uploadResult,
|
|
}: {
|
|
readonly client: ReturnType<typeof createAdminApiClient>;
|
|
readonly knowledgeSpaceId: string;
|
|
readonly searchParams: Record<string, string | string[] | undefined>;
|
|
readonly token: string;
|
|
readonly uploadResult: AdminUploadResult | null;
|
|
}): Promise<AdminPublishReadiness> {
|
|
const requestedDocumentId =
|
|
firstSearchParam(searchParams.documentId) ||
|
|
(uploadResult?.status === "success" ? uploadResult.documentId : undefined);
|
|
const asset = requestedDocumentId
|
|
? await client.getDocument({
|
|
documentId: requestedDocumentId,
|
|
knowledgeSpaceId,
|
|
token,
|
|
})
|
|
: (await client.listDocuments({ knowledgeSpaceId, limit: 1, token })).items[0];
|
|
|
|
if (!asset) {
|
|
return createPublishReadiness(uploadResult);
|
|
}
|
|
|
|
const artifact =
|
|
asset.parserStatus === "parsed"
|
|
? await client
|
|
.getParseArtifact({
|
|
documentId: asset.id,
|
|
knowledgeSpaceId,
|
|
token,
|
|
version: asset.version,
|
|
})
|
|
.catch(() => undefined)
|
|
: undefined;
|
|
|
|
return createPublishReadinessFromDocument({ artifact, asset });
|
|
}
|
|
|
|
function selectActiveWorkspace(
|
|
workspaceOptions: readonly AdminWorkspaceOption[],
|
|
searchParams: Record<string, string | string[] | undefined>,
|
|
uploadResult: AdminUploadResult | null,
|
|
): AdminWorkspaceOption {
|
|
const resolved = workspaceOptions.length > 0 ? workspaceOptions : [fallbackWorkspaceOption];
|
|
const requestedId =
|
|
firstSearchParam(searchParams.spaceId) ||
|
|
(uploadResult?.status === "success" ? uploadResult.spaceId : undefined);
|
|
|
|
return (
|
|
resolved.find((workspace) => workspace.id === requestedId) ??
|
|
resolved.find((workspace) => workspace.slug === "workspace") ??
|
|
resolved[0] ??
|
|
fallbackWorkspaceOption
|
|
);
|
|
}
|
|
|
|
function createRetrievalResult(queryResult: AdminQueryResult | null): {
|
|
readonly answer: string;
|
|
readonly citations: string;
|
|
readonly confidence: string;
|
|
readonly freshness: string;
|
|
readonly query: string;
|
|
readonly subtitle: string;
|
|
readonly traceId?: string | undefined;
|
|
} {
|
|
if (queryResult?.status === "success") {
|
|
return {
|
|
answer: queryResult.answer,
|
|
citations: queryResult.citations.length > 0 ? queryResult.citations.join(", ") : "None",
|
|
confidence: queryResult.confidence,
|
|
freshness: queryResult.freshness,
|
|
query: queryResult.query,
|
|
subtitle: "Latest query result",
|
|
...(queryResult.traceId ? { traceId: queryResult.traceId } : {}),
|
|
};
|
|
}
|
|
|
|
if (queryResult?.status === "error") {
|
|
return {
|
|
answer: "Query did not complete.",
|
|
citations: "None",
|
|
confidence: "Unavailable",
|
|
freshness: "Unavailable",
|
|
query: "",
|
|
subtitle: "Latest query failed",
|
|
};
|
|
}
|
|
|
|
return {
|
|
answer: "No query has been run in this Admin session.",
|
|
citations: "None",
|
|
confidence: "Unavailable",
|
|
freshness: "Unavailable",
|
|
query: "",
|
|
subtitle: "No live query yet",
|
|
};
|
|
}
|
|
|
|
function createHealthItems(health: AdminHealthStatus | null): readonly AdminHealthMetric[] {
|
|
if (!health) {
|
|
return [
|
|
unavailableHealthMetric("Gateway"),
|
|
unavailableHealthMetric("Database"),
|
|
unavailableHealthMetric("Object store"),
|
|
unavailableHealthMetric("Parser"),
|
|
unavailableHealthMetric("Reranker"),
|
|
];
|
|
}
|
|
|
|
return [
|
|
{
|
|
badge: formatRuntime(health.runtime),
|
|
label: "Gateway",
|
|
status: health.ok ? "ok" : "warn",
|
|
value: health.ok ? "Online" : "Degraded",
|
|
},
|
|
componentMetric({
|
|
healthy: health.components.database,
|
|
label: "Database",
|
|
okValue: "Indexed",
|
|
}),
|
|
componentMetric({
|
|
healthy: health.components.objectStorage,
|
|
label: "Object store",
|
|
okValue: "Writable",
|
|
}),
|
|
componentMetric({
|
|
healthy: health.components.parser,
|
|
label: "Parser",
|
|
okValue: "Ready",
|
|
}),
|
|
componentMetric({
|
|
fallbackValue: "Fallback",
|
|
healthy: health.components.reranker,
|
|
label: "Reranker",
|
|
okValue: "Ready",
|
|
warnWhenUnhealthy: true,
|
|
}),
|
|
];
|
|
}
|
|
|
|
function createPublishReadiness(uploadResult: AdminUploadResult | null): AdminPublishReadiness {
|
|
if (!uploadResult) {
|
|
return {
|
|
badge: "unavailable",
|
|
elementCount: "Unavailable",
|
|
fileSize: "Unavailable",
|
|
nodeCount: "Unavailable",
|
|
parserStatus: "No document selected",
|
|
qualityRisks: "Upload a document to inspect readiness",
|
|
status: "warn",
|
|
subtitle: "No live document selected",
|
|
};
|
|
}
|
|
|
|
if (uploadResult.status === "error") {
|
|
return {
|
|
badge: "failed",
|
|
elementCount: "Unavailable",
|
|
fileSize: "Unavailable",
|
|
nodeCount: "Unavailable",
|
|
parserStatus: "Upload failed",
|
|
qualityRisks: uploadResult.error,
|
|
status: "fail",
|
|
subtitle: "Latest upload",
|
|
};
|
|
}
|
|
|
|
return {
|
|
badge: uploadResult.parserStatus === "parsed" ? "ready" : uploadResult.parserStatus,
|
|
elementCount: "Open parse artifact",
|
|
fileSize: formatBytes(uploadResult.sizeBytes),
|
|
nodeCount: "Open document status",
|
|
parserStatus: uploadResult.parserStatus,
|
|
qualityRisks: uploadResult.parserStatus === "failed" ? "Parser reported failure" : "None",
|
|
status:
|
|
uploadResult.parserStatus === "parsed"
|
|
? "ready"
|
|
: uploadResult.parserStatus === "failed"
|
|
? "fail"
|
|
: "review",
|
|
subtitle: "Latest upload",
|
|
};
|
|
}
|
|
|
|
function createPublishReadinessFromDocument({
|
|
artifact,
|
|
asset,
|
|
}: {
|
|
readonly artifact?: AdminParseArtifact | undefined;
|
|
readonly asset: AdminDocumentAsset;
|
|
}): AdminPublishReadiness {
|
|
const report = createDocumentHealthReport({
|
|
...(artifact ? { artifact } : {}),
|
|
asset,
|
|
nodeCount: artifact?.elements.length ?? 0,
|
|
});
|
|
const qualityRisks = report.qualityRisks.length > 0 ? report.qualityRisks.join("; ") : "None";
|
|
|
|
return {
|
|
badge: report.publishReadiness,
|
|
elementCount: artifact ? String(report.elementCount) : "Parse artifact unavailable",
|
|
fileSize: report.sizeLabel,
|
|
nodeCount: artifact
|
|
? String(report.nodeCount)
|
|
: asset.parserStatus === "pending"
|
|
? "Pending"
|
|
: "Unavailable",
|
|
parserStatus: report.parserStatus,
|
|
qualityRisks,
|
|
status:
|
|
report.publishReadiness === "ready"
|
|
? "ready"
|
|
: report.publishReadiness === "blocked"
|
|
? "fail"
|
|
: "review",
|
|
subtitle: `Latest document: ${report.documentName}`,
|
|
};
|
|
}
|
|
|
|
function createEvaluationStats(list: AdminGoldenQuestionList | undefined): {
|
|
readonly annotated: number;
|
|
readonly badCases: number;
|
|
readonly pending: number;
|
|
readonly total: number;
|
|
} {
|
|
const items = list?.items ?? [];
|
|
const annotated = items.filter(
|
|
(item) => item.tags.includes("annotated") || item.metadata.annotation !== undefined,
|
|
).length;
|
|
const badCases = items.filter(
|
|
(item) =>
|
|
item.tags.includes("production-bad-case") || item.metadata.source === "production-bad-case",
|
|
).length;
|
|
|
|
return {
|
|
annotated,
|
|
badCases,
|
|
pending: Math.max(0, items.length - annotated),
|
|
total: items.length,
|
|
};
|
|
}
|
|
|
|
function createSemanticViewStatus(liveData: AdminLiveData): {
|
|
readonly badge: string;
|
|
readonly badgeStatus: "fail" | "ok" | "warn";
|
|
readonly description: string;
|
|
} {
|
|
if (!liveData.topicView && !liveData.entityView && !liveData.communityView) {
|
|
return {
|
|
badge: "Unavailable",
|
|
badgeStatus: "fail",
|
|
description: "Semantic views could not be loaded from KnowledgeFS",
|
|
};
|
|
}
|
|
|
|
const topicCount = liveData.topicView?.items.length ?? 0;
|
|
const entityCount = liveData.entityView?.items.length ?? 0;
|
|
const communityCount = liveData.communityView?.items.length ?? 0;
|
|
|
|
if (topicCount === 0 && entityCount === 0 && communityCount === 0) {
|
|
return {
|
|
badge: "Not materialized",
|
|
badgeStatus: "warn",
|
|
description: "KnowledgeFS is reachable, but semantic materialization has no entries yet",
|
|
};
|
|
}
|
|
|
|
return {
|
|
badge: "Live",
|
|
badgeStatus: "ok",
|
|
description: "Live topic, entity, and community views from KnowledgeFS",
|
|
};
|
|
}
|
|
|
|
function componentMetric({
|
|
fallbackValue = "Unavailable",
|
|
healthy,
|
|
label,
|
|
okValue,
|
|
warnWhenUnhealthy = false,
|
|
}: {
|
|
readonly fallbackValue?: string;
|
|
readonly healthy: boolean | undefined;
|
|
readonly label: string;
|
|
readonly okValue: string;
|
|
readonly warnWhenUnhealthy?: boolean;
|
|
}): AdminHealthMetric {
|
|
if (healthy === true) {
|
|
return {
|
|
badge: "ok",
|
|
label,
|
|
status: "ok",
|
|
value: okValue,
|
|
};
|
|
}
|
|
|
|
return {
|
|
badge: warnWhenUnhealthy ? "warn" : "fail",
|
|
label,
|
|
status: warnWhenUnhealthy ? "warn" : "fail",
|
|
value: fallbackValue,
|
|
};
|
|
}
|
|
|
|
function unavailableHealthMetric(label: string): AdminHealthMetric {
|
|
return {
|
|
badge: "fail",
|
|
label,
|
|
status: "fail",
|
|
value: "Unavailable",
|
|
};
|
|
}
|
|
|
|
function formatRuntime(runtime: AdminHealthStatus["runtime"]): string {
|
|
return runtime === "node-docker" ? "Node Docker" : "Cloudflare";
|
|
}
|
|
|
|
function formatStatusNumber(
|
|
record: Readonly<Record<string, unknown>> | undefined,
|
|
key: string,
|
|
): string {
|
|
const value = record?.[key];
|
|
|
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : "Unavailable";
|
|
}
|
|
|
|
function formatFsckSummary(
|
|
report: AdminKnowledgeFsckReport | undefined,
|
|
key: "error" | "repairable" | "scanned" | "warning",
|
|
): string {
|
|
const value = report?.summary[key];
|
|
|
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : "Unavailable";
|
|
}
|
|
|
|
function formatGcSummary(
|
|
report: AdminKnowledgeFsGcDryRunReport | undefined,
|
|
key: "candidateCount" | "estimatedBytes",
|
|
): string {
|
|
const value = report?.summary[key];
|
|
|
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : "Unavailable";
|
|
}
|
|
|
|
function shortId(value: string): string {
|
|
return value.length > 12 ? value.slice(0, 12) : value;
|
|
}
|
|
|
|
function parseKnowledgeFsCommand(value: string | undefined): AdminKnowledgeFsCommand {
|
|
return knowledgeFsCommands.find((command) => command === value) ?? "ls";
|
|
}
|
|
|
|
function parseFormPositiveInteger(value: string, fallback: number): number {
|
|
const parsed = Number.parseInt(value, 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
function formatJson(value: Readonly<Record<string, unknown>>): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function metadataString(
|
|
metadata: Readonly<Record<string, unknown>>,
|
|
key: string,
|
|
): string | undefined {
|
|
const value = metadata[key];
|
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
}
|
|
|
|
function metadataNumber(
|
|
metadata: Readonly<Record<string, unknown>>,
|
|
key: string,
|
|
): number | undefined {
|
|
const value = metadata[key];
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
function titleizeSlug(value: string): string {
|
|
return value
|
|
.split("-")
|
|
.filter(Boolean)
|
|
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
.join(" ");
|
|
}
|
|
|
|
function formatTopicKind(kind: AdminKnowledgeFsEntry["kind"]): string {
|
|
return kind === "directory" ? "Document collection" : "Document";
|
|
}
|
|
|
|
function formatEntityType(value: string): string {
|
|
return titleizeSlug(value.toLocaleLowerCase());
|
|
}
|
|
|
|
function formatEntryCount(count: number, singular: string): string {
|
|
return `${count} ${singular}${count === 1 ? "" : "s"}`;
|
|
}
|
|
|
|
function formatOptionalCounts(values: readonly (string | undefined)[]): string {
|
|
const formatted = values.filter((value): value is string => Boolean(value));
|
|
|
|
return formatted.length > 0 ? formatted.join(" / ") : "directory";
|
|
}
|
|
|
|
function isReadableSemanticEntityEntry(entry: AdminKnowledgeFsEntry): boolean {
|
|
const type = metadataString(entry.metadata, "type");
|
|
if (type === "date") {
|
|
return /^(?:19|20)\d{2}(?:-\d{2}-\d{2})?$/.test(entry.name);
|
|
}
|
|
|
|
if (type === "metric") {
|
|
return !isBareNumber(entry.name);
|
|
}
|
|
|
|
return !isBareNumber(entry.name);
|
|
}
|
|
|
|
function isBareNumber(value: string): boolean {
|
|
return /^\d+(?:\.\d+)?$/.test(value.trim());
|
|
}
|
|
|
|
function fsckIssueKey(issue: AdminKnowledgeFsckReport["issues"][number], index: number): string {
|
|
const targetKey = [
|
|
issue.target.id,
|
|
issue.target.objectKey,
|
|
issue.target.documentAssetId,
|
|
issue.target.parseArtifactId,
|
|
issue.target.virtualPath,
|
|
]
|
|
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
.join(":");
|
|
|
|
return `${issue.type}:${issue.code}:${targetKey}:${index}`;
|
|
}
|
|
|
|
function formatPercent(value: number): string {
|
|
return `${Math.round(value * 100)}%`;
|
|
}
|
|
|
|
async function loadAdminHealth(): Promise<AdminHealthStatus | null> {
|
|
try {
|
|
const client = createAdminApiClient({ baseUrl: getAdminApiBase() });
|
|
return await client.health();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function loadAdminWorkspaceOptions(): Promise<AdminWorkspaceOption[]> {
|
|
const token = getAdminServerToken();
|
|
if (!token) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const client = createAdminApiClient({ baseUrl: getAdminApiBase() });
|
|
const result = await client.listKnowledgeSpaces({ limit: 100, token });
|
|
|
|
return result.items.map(toWorkspaceOption);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function parseUploadResult(
|
|
searchParams: Record<string, string | string[] | undefined>,
|
|
): AdminUploadResult | null {
|
|
const status = firstSearchParam(searchParams.uploadStatus);
|
|
if (status === "error") {
|
|
return {
|
|
error: firstSearchParam(searchParams.uploadError) ?? "Document upload failed",
|
|
status: "error",
|
|
};
|
|
}
|
|
|
|
if (status !== "success") {
|
|
return null;
|
|
}
|
|
|
|
const documentId = firstSearchParam(searchParams.documentId);
|
|
const filename = firstSearchParam(searchParams.filename);
|
|
const parserStatus = firstSearchParam(searchParams.parserStatus);
|
|
const sha256 = firstSearchParam(searchParams.sha256);
|
|
const sizeBytes = Number(firstSearchParam(searchParams.sizeBytes));
|
|
const spaceId = firstSearchParam(searchParams.spaceId);
|
|
const version = Number(firstSearchParam(searchParams.version));
|
|
|
|
if (
|
|
!documentId ||
|
|
!filename ||
|
|
!spaceId ||
|
|
!sha256 ||
|
|
!Number.isInteger(sizeBytes) ||
|
|
sizeBytes < 0 ||
|
|
!Number.isInteger(version) ||
|
|
version < 1 ||
|
|
(parserStatus !== "pending" && parserStatus !== "parsed" && parserStatus !== "failed")
|
|
) {
|
|
return {
|
|
error: "Document upload response is invalid",
|
|
status: "error",
|
|
};
|
|
}
|
|
|
|
return {
|
|
documentId,
|
|
filename,
|
|
parserStatus,
|
|
sha256,
|
|
sizeBytes,
|
|
spaceId,
|
|
status: "success",
|
|
version,
|
|
};
|
|
}
|
|
|
|
function parseQueryResult(
|
|
searchParams: Record<string, string | string[] | undefined>,
|
|
): AdminQueryResult | null {
|
|
const status = firstSearchParam(searchParams.queryStatus);
|
|
if (status === "error") {
|
|
return {
|
|
error: firstSearchParam(searchParams.queryError) ?? "Query failed",
|
|
status: "error",
|
|
};
|
|
}
|
|
|
|
if (status !== "success") {
|
|
return null;
|
|
}
|
|
|
|
const answer = firstSearchParam(searchParams.answer);
|
|
const query = firstSearchParam(searchParams.query);
|
|
if (!answer || !query) {
|
|
return {
|
|
error: "Query response is invalid",
|
|
status: "error",
|
|
};
|
|
}
|
|
|
|
return {
|
|
answer,
|
|
citations: splitCsv(firstSearchParam(searchParams.citations) ?? ""),
|
|
confidence: firstSearchParam(searchParams.confidence) ?? "Generated",
|
|
freshness: firstSearchParam(searchParams.freshness) ?? "Live query",
|
|
query,
|
|
status: "success",
|
|
...(firstSearchParam(searchParams.traceId)
|
|
? { traceId: firstSearchParam(searchParams.traceId) }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function parseAdminNotice(
|
|
searchParams: Record<string, string | string[] | undefined>,
|
|
): AdminNotice | undefined {
|
|
const status = firstSearchParam(searchParams.adminStatus);
|
|
const message = firstSearchParam(searchParams.adminMessage);
|
|
|
|
if ((status !== "error" && status !== "success") || !message) {
|
|
return undefined;
|
|
}
|
|
|
|
return { message, status };
|
|
}
|
|
|
|
function firstSearchParam(value: string | string[] | undefined): string | undefined {
|
|
if (Array.isArray(value)) {
|
|
return value[0];
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function isUuid(value: string): boolean {
|
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
}
|
|
|
|
function splitCsv(value: string): string[] {
|
|
return value
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function toWorkspaceOption(space: AdminKnowledgeSpace): AdminWorkspaceOption {
|
|
return {
|
|
id: space.id,
|
|
name: space.name,
|
|
slug: space.slug,
|
|
};
|
|
}
|