mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-06 23:44:36 +00:00
p2 (Pi Mobile on Chord v2) moves from pi-private into pi as one commit. Root scripts that loop all workspaces use --if-present; npm test, ./test.sh and the release script cover packages/* only (p2: cd apps/p2 && npm test); sync-versions also updates apps' workspace pins.
111 lines
3.2 KiB
TypeScript
111 lines
3.2 KiB
TypeScript
import { compile, obj, union } from "@earendil-works/pi-schema";
|
|
import { contribute, defineFacet, literal } from "@p2/app/v1";
|
|
import {
|
|
ArtifactIcon,
|
|
ArtifactPicker,
|
|
closeLayers,
|
|
FilePanel,
|
|
OpenViewer,
|
|
openLayer,
|
|
SessionId,
|
|
Slots,
|
|
useObject,
|
|
useReadable,
|
|
useRoute,
|
|
type ViewerItem,
|
|
} from "@p2/app/v1/web";
|
|
import type { ReactNode } from "react";
|
|
import { DownloadInfoSchema } from "../files/contract.ts";
|
|
import { ArtifactInfoSchema, ArtifactList } from "./contract.ts";
|
|
import { ArtifactPanel } from "./preview.tsx";
|
|
|
|
const ViewerSchema = union([
|
|
obj({ kind: literal("file"), file: DownloadInfoSchema }, { open: true }),
|
|
obj({ kind: literal("artifact"), artifact: ArtifactInfoSchema }, { open: true }),
|
|
]);
|
|
const isViewer = compile(ViewerSchema).check;
|
|
|
|
/** Untrusted URL metadata is only a locator. HTTP routes authorize every content fetch. */
|
|
function viewer(overlay: string | undefined): ViewerItem | undefined {
|
|
if (!overlay?.startsWith("viewer:")) return undefined;
|
|
try {
|
|
const value: unknown = JSON.parse(overlay.slice("viewer:".length));
|
|
return isViewer(value) ? value : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function Content({ children }: { children?: ReactNode }) {
|
|
const route = useRoute();
|
|
const list = useObject(
|
|
ArtifactList,
|
|
route.session ? { session: route.session, conversation: route.conversation ?? 1 } : undefined,
|
|
);
|
|
const artifacts = useReadable(list?.artifacts) ?? [];
|
|
|
|
const top = route.layers.at(-1);
|
|
const item = viewer(top?.kind === "viewer" && top.name === "artifacts" ? top.locator : undefined);
|
|
const open = (item: ViewerItem): void =>
|
|
openLayer({ kind: "viewer", name: "artifacts", locator: `viewer:${JSON.stringify(item)}` });
|
|
return (
|
|
<SessionId.Provider value={route.session}>
|
|
<OpenViewer.Provider value={open}>
|
|
{children}
|
|
{top?.kind === "sheet" && top.name === "artifacts" && (
|
|
<ArtifactPicker
|
|
artifacts={artifacts.toReversed()}
|
|
onOpen={(artifact) => open({ kind: "artifact", artifact })}
|
|
onClose={closeLayers}
|
|
/>
|
|
)}
|
|
|
|
{route.session &&
|
|
item &&
|
|
(item.kind === "artifact" ? (
|
|
<ArtifactPanel
|
|
key={top?.kind !== "drawer" ? top?.locator : undefined}
|
|
sessionId={route.session}
|
|
artifact={item.artifact}
|
|
onClose={closeLayers}
|
|
/>
|
|
) : (
|
|
<FilePanel
|
|
key={top?.kind !== "drawer" ? top?.locator : undefined}
|
|
sessionId={route.session}
|
|
file={item.file}
|
|
onClose={closeLayers}
|
|
/>
|
|
))}
|
|
</OpenViewer.Provider>
|
|
</SessionId.Provider>
|
|
);
|
|
}
|
|
|
|
function ArtifactButton() {
|
|
const route = useRoute();
|
|
const list = useObject(
|
|
ArtifactList,
|
|
route.session ? { session: route.session, conversation: route.conversation ?? 1 } : undefined,
|
|
);
|
|
const artifacts = useReadable(list?.artifacts) ?? [];
|
|
const count = artifacts.length;
|
|
if (!count) return null;
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="artifact-button"
|
|
aria-label={`Artifacts (${count})`}
|
|
onClick={() => openLayer({ kind: "sheet", name: "artifacts" })}
|
|
>
|
|
<ArtifactIcon />
|
|
<span>{count}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default defineFacet((host) => {
|
|
contribute(host, "p2.web-artifacts", (self) => {
|
|
self.contribute(Slots, { "chat-content": Content, "artifact-actions": ArtifactButton });
|
|
});
|
|
});
|