diff --git a/README.md b/README.md index 4223429..d2a988f 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ it in three places: "@crema/knowledge-ui": libSrc("knowledge-ui") + "/index.tsx", "@crema/knowledge-ui/": libSrc("knowledge-ui") + "/", ``` - The lib also imports `@crema/file-ui` (for uploads/previews), so alias that too - if not already, and make sure `lucide-react` is in your shared-dep dedupe list. + Make sure `lucide-react` is in your shared-dep dedupe list. 2. **`tsconfig.json`** — paths: ```json @@ -34,6 +33,16 @@ it in three places: @source "../../lib-knowledge-ui/src"; ``` +The lib is **fully self-contained** — it imports only `react` and `lucide-react` +(no other `@crema/*` libs), so a fresh consumer wires exactly this one alias. The +app may separately use `@crema/file-ui` for the upload dropzone on its collection +page (spec §4.5), but that's the app's choice, not a lib dependency. + +The vite alias (step 1) also needs its own step — mirror step 2 in +`vite.config.ts` `resolve.alias`, and ensure `react` / `react-dom` / +`lucide-react` are deduped to the app's copies (sibling libs carry no +node_modules). + ## Wiring the transport The lib defines `KnowledgeTransport`; the app implements it over its KB client diff --git a/demo/knowledge.tsx b/demo/knowledge.tsx index 3d6a863..e2ae29f 100644 --- a/demo/knowledge.tsx +++ b/demo/knowledge.tsx @@ -7,6 +7,7 @@ import { ClaimsReviewQueue, CollectionForm, CollectionList, + ExportPanel, ObjectList, ObjectViewer, MockKnowledgeTransport, @@ -14,6 +15,8 @@ import { type Collection, type CollectionInput, type CollectionPatch, + type ExportJob, + type ExportScope, type KnowledgeTransport, type ObjectOutline, type ObjectSummary, @@ -27,7 +30,9 @@ type View = | { kind: "object"; id: string }; export default function KnowledgeDemo() { - const [tab, setTab] = useState<"browse" | "review">("browse"); + const [tab, setTab] = useState<"browse" | "review" | "export">("browse"); + const [exports, setExports] = useState([]); + const [exportBusy, setExportBusy] = useState(false); const [view, setView] = useState({ kind: "collections" }); const [collections, setCollections] = useState([]); const [pending, setPending] = useState>({}); @@ -56,6 +61,23 @@ export default function KnowledgeDemo() { void refresh(); } + // App-side polling of in-flight exports (the timer lives here, not in the lib). + async function requestExport(scope: ExportScope) { + setExportBusy(true); + const job = await transport.createExport(scope); + setExports((prev) => [job, ...prev]); + setExportBusy(false); + poll(job.id); + } + function poll(id: string) { + const tick = async () => { + const job = await transport.getExport(id); + setExports((prev) => prev.map((e) => (e.id === id ? job : e))); + if (job.status === "pending" || job.status === "running") setTimeout(tick, 1500); + }; + setTimeout(tick, 800); + } + useEffect(() => { void refresh(); }, []); @@ -90,8 +112,19 @@ export default function KnowledgeDemo() { setTab("review")}> Review{reviewCount > 0 ? ` (${reviewCount})` : ""} + setTab("export")}>Export + {tab === "export" && ( + job.download_url && window.open(job.download_url)} + /> + )} + {tab === "review" && ( void; className?: string }> = ({ + job, + onDownload, + className, +}) => { + const running = job.status === "pending" || job.status === "running"; + return ( +
+ +
+

+ {job.scope === "all" ? "Whole knowledge base" : job.scope} +

+

+ {running + ? "Building…" + : job.status === "ready" + ? `${job.object_count ?? 0} items · ${formatBytes(job.byte_size)} · ${formatRelative(job.updated_at)}` + : job.error || "Failed"} +

+
+ {job.status === "ready" && onDownload && ( + + )} + {running && } +
+ ); +}; + +const StatusIcon: FC<{ status: ExportJob["status"] }> = ({ status }) => { + if (status === "ready") return ; + if (status === "failed") return ; + return ; +}; + +// ---- ExportPanel ---------------------------------------------------------- + +export interface ExportPanelProps { + /** Corpuses the caller may export (for the scope picker). */ + collections: Collection[]; + jobs: ExportJob[]; + busy?: boolean; + onRequest: (scope: ExportScope) => void; + onDownload?: (job: ExportJob) => void; + intro?: ReactNode; + className?: string; +} + +export const ExportPanel: FC = ({ collections, jobs, busy, onRequest, onDownload, intro, className }) => { + const [scope, setScope] = useState("all"); + + return ( +
+
+

Export a bundle

+

+ {intro ?? ( + <> + A self-contained zip — your originals, extracted text, catalog cards, and claims, in open formats. + Citations resolve inside the bundle, so it stays readable with no service in the loop. It's yours to keep. + + )} +

+
+ + +
+
+ + {jobs.length > 0 && ( +
+

Exports

+ {jobs.map((j) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/tsconfig.check.json b/tsconfig.check.json index 3da7cd9..a9296a1 100644 --- a/tsconfig.check.json +++ b/tsconfig.check.json @@ -16,8 +16,7 @@ "react": ["../arcadia-personal-cloud-web/node_modules/@types/react/index.d.ts"], "react/jsx-runtime": ["../arcadia-personal-cloud-web/node_modules/@types/react/jsx-runtime.d.ts"], "react-dom": ["../arcadia-personal-cloud-web/node_modules/@types/react-dom/index.d.ts"], - "lucide-react": ["../arcadia-personal-cloud-web/node_modules/lucide-react/dist/lucide-react.d.ts"], - "@crema/file-ui": ["../lib-file-ui/src/index.tsx"] + "lucide-react": ["../arcadia-personal-cloud-web/node_modules/lucide-react/dist/lucide-react.d.ts"] } }, "include": ["src", "demo"]