mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
The artifact tool takes inline: true to also show the produced version below its transcript card: Markdown/text rendered, HTML/SVG in the sandboxed preview frame. Drawings fill the frame's width (an SVG with a viewBox or an absolute size scales to it; an HTML page that is only an SVG or image too) and the frame takes the content height the preview reports, capped at 70vh. Every viewer sees it, guests included (owner decision). The TUI renders inline Markdown and text. SVG previews now carry the diagnostics bridge in CDATA (its && broke the XML).
112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
import { compile, obj, union } from "@earendil-works/pi-schema";
|
|
import { contribute, defineFacet, literal } from "@p2/app/v1";
|
|
import {
|
|
ArtifactIcon,
|
|
ArtifactPicker,
|
|
closeLayers,
|
|
FilePanel,
|
|
InlineArtifactView,
|
|
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, InlineArtifact } 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}>
|
|
<InlineArtifactView.Provider value={InlineArtifact}>{children}</InlineArtifactView.Provider>
|
|
{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 });
|
|
});
|
|
});
|