W4: object viewer
components-object: ObjectList, ObjectViewer (CatalogCard + ProvenancePanel + SupersessionBanner + OutlineNav + TextReader with citation highlight/scroll + load-more, image render, vault notice, action bar), CitationLink + parseCite. Demo browses collections → objects → viewer. Typechecks clean. Co-Authored-By: Claude Fable 5 (build) <noreply@anthropic.com>
This commit is contained in:
@@ -6,18 +6,30 @@ import { useEffect, useState } from "react";
|
||||
import {
|
||||
CollectionForm,
|
||||
CollectionList,
|
||||
ObjectList,
|
||||
ObjectViewer,
|
||||
MockKnowledgeTransport,
|
||||
type Collection,
|
||||
type CollectionInput,
|
||||
type CollectionPatch,
|
||||
type KnowledgeTransport,
|
||||
type ObjectOutline,
|
||||
type ObjectSummary,
|
||||
} from "../src/index";
|
||||
|
||||
const transport: KnowledgeTransport = new MockKnowledgeTransport();
|
||||
|
||||
type View =
|
||||
| { kind: "collections" }
|
||||
| { kind: "collection"; slug: string }
|
||||
| { kind: "object"; id: string };
|
||||
|
||||
export default function KnowledgeDemo() {
|
||||
const [view, setView] = useState<View>({ kind: "collections" });
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [pending, setPending] = useState<Record<string, number>>({});
|
||||
const [objects, setObjects] = useState<ObjectSummary[]>([]);
|
||||
const [outline, setOutline] = useState<ObjectOutline | null>(null);
|
||||
const [creating, setCreating] = useState<null | "account" | "tenant">(null);
|
||||
const [editing, setEditing] = useState<Collection | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -35,6 +47,11 @@ export default function KnowledgeDemo() {
|
||||
void refresh();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (view.kind === "collection") void transport.listObjects(view.slug).then((p) => setObjects(p.objects));
|
||||
if (view.kind === "object") void transport.outline(view.id).then(setOutline);
|
||||
}, [view]);
|
||||
|
||||
async function onCreate(value: CollectionInput | CollectionPatch) {
|
||||
setBusy(true);
|
||||
await transport.createCollection(value as CollectionInput);
|
||||
@@ -42,7 +59,6 @@ export default function KnowledgeDemo() {
|
||||
setCreating(null);
|
||||
void refresh();
|
||||
}
|
||||
|
||||
async function onEdit(value: CollectionInput | CollectionPatch) {
|
||||
if (!editing) return;
|
||||
setBusy(true);
|
||||
@@ -54,33 +70,61 @@ export default function KnowledgeDemo() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl p-6">
|
||||
<header className="mb-6">
|
||||
<h1 className="text-xl font-semibold text-foreground">Knowledge</h1>
|
||||
<p className="text-sm text-muted-foreground">Your corpuses — browse, organise, and review.</p>
|
||||
</header>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView({ kind: "collections" })}
|
||||
className="mb-4 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Knowledge{view.kind !== "collections" ? " / …" : ""}
|
||||
</button>
|
||||
|
||||
{creating && (
|
||||
<div className="mb-6 rounded-xl border border-border bg-card p-4">
|
||||
<h2 className="mb-3 font-medium">New corpus</h2>
|
||||
<CollectionForm mode="create" ownerType={creating} orgName="Acme" canManageTenant busy={busy} onSubmit={onCreate} onCancel={() => setCreating(null)} />
|
||||
</div>
|
||||
{view.kind === "collections" && (
|
||||
<>
|
||||
{creating && (
|
||||
<div className="mb-6 rounded-xl border border-border bg-card p-4">
|
||||
<h2 className="mb-3 font-medium">New corpus</h2>
|
||||
<CollectionForm mode="create" ownerType={creating} orgName="Acme" canManageTenant busy={busy} onSubmit={onCreate} onCancel={() => setCreating(null)} />
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<div className="mb-6 rounded-xl border border-border bg-card p-4">
|
||||
<h2 className="mb-3 font-medium">Corpus settings</h2>
|
||||
<CollectionForm mode="edit" initial={editing} orgName="Acme" canManageTenant busy={busy} onSubmit={onEdit} onCancel={() => setEditing(null)} />
|
||||
</div>
|
||||
)}
|
||||
<CollectionList
|
||||
collections={collections}
|
||||
pendingCounts={pending}
|
||||
orgName="Acme"
|
||||
canManageTenant
|
||||
onCreate={(owner) => setCreating(owner)}
|
||||
onOpen={(c) => setView({ kind: "collection", slug: c.slug })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<div className="mb-6 rounded-xl border border-border bg-card p-4">
|
||||
<h2 className="mb-3 font-medium">Corpus settings</h2>
|
||||
<CollectionForm mode="edit" initial={editing} orgName="Acme" canManageTenant busy={busy} onSubmit={onEdit} onCancel={() => setEditing(null)} />
|
||||
</div>
|
||||
{view.kind === "collection" && (
|
||||
<>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h1 className="text-lg font-semibold">{view.slug}</h1>
|
||||
<button className="text-sm text-primary" onClick={() => setEditing(collections.find((c) => c.slug === view.slug) ?? null)}>Settings</button>
|
||||
</div>
|
||||
<ObjectList objects={objects} onOpen={(o) => setView({ kind: "object", id: o.object_id })} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<CollectionList
|
||||
collections={collections}
|
||||
pendingCounts={pending}
|
||||
orgName="Acme"
|
||||
canManageTenant
|
||||
onCreate={(owner) => setCreating(owner)}
|
||||
onOpen={(c) => setEditing(c)}
|
||||
/>
|
||||
{view.kind === "object" && outline && (
|
||||
<ObjectViewer
|
||||
outline={outline}
|
||||
readText={(sel) => transport.readText(outline.object_id, sel)}
|
||||
resolveBlobUrl={() => transport.blobUrl(outline.object_id)}
|
||||
onOpenObject={(id) => setView({ kind: "object", id })}
|
||||
actions={{
|
||||
onVerify: () => transport.updateObject(outline.object_id, { verified_at: new Date().toISOString() }),
|
||||
onDownload: () => transport.blobUrl(outline.object_id).then((u) => window.open(u)),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user