import type { OpenGeniClient, ToolGatewayIdentity } from "@opengeni/sdk"; import { useEffect, useMemo, useState } from "react"; import { loadSiteSnapshot } from "../../sites-ui"; import { useOpenGeniLinkResolver } from "../open-geni-links"; import { ArtifactButton, ArtifactLabelsProvider, ArtifactSelect, useArtifactLabels, type ArtifactLabels, } from "./artifact-chrome"; import { ArtifactSandbox } from "./artifact-sandbox"; import { DeferredChatMedia } from "./deferred-chat-media"; import { inlineHtmlDocument } from "./inline-html-document"; import type { PublishedHtmlArtifactToolBridge } from "./published-html-artifact-frame"; // Fixed chat viewports keep async Site loads and fragment resize messages from // moving the conversation. Full-screen remains available for larger content. const INLINE_PREVIEW_HEIGHT = 360; const SITE_PREVIEW_HEIGHT = 400; const PREVIEW_CHROME_HEIGHT = 50; /** Reads a Site needs to render; the console client, the SDK client, or a proxy. */ export type SiteSnapshotClient = Pick< OpenGeniClient, "getWorkspaceArtifact" | "getWorkspaceArtifactHtml" > & Partial>; /** The exact saved Site a tool bridge is pinned to (absent for inline HTML). */ export type SiteToolScope = { artifactId: string; siteVersionId: string; requestedTools: readonly ToolGatewayIdentity[]; }; /** Host-owned Site API access. Omit it and Sites render without workspace tools. */ export type SiteToolBridgeFactory = ( site?: SiteToolScope, ) => PublishedHtmlArtifactToolBridge | undefined; export type ChatInteractiveBlockProps = { workspaceId: string; kind: "html" | "site"; content: string; client: SiteSnapshotClient; toolBridge?: SiteToolBridgeFactory | undefined; theme?: "light" | "dark" | undefined; /** Translations for the preview's copy; defaults to English. */ labels?: Partial | undefined; }; /** * Inline preview for an assistant `opengeni-html` or `opengeni-site` fence, as * the Opengeni console renders it. "Open Site" goes through the nearest * `resolveLink`, so a host decides where a Site opens. */ export function ChatInteractiveBlock({ labels, ...props }: ChatInteractiveBlockProps) { return ( ); } function InteractiveBlock(props: ChatInteractiveBlockProps) { const labels = useArtifactLabels(); const height = (props.kind === "html" ? INLINE_PREVIEW_HEIGHT : SITE_PREVIEW_HEIGHT) + PREVIEW_CHROME_HEIGHT; return (
); } function LoadedChatInteractiveBlock(props: ChatInteractiveBlockProps) { const labels = useArtifactLabels(); if (props.kind === "html") return ; try { const value = JSON.parse(props.content); if ( !value || typeof value.siteId !== "string" || !/^[0-9a-f-]{36}$/i.test(value.siteId) || (value.versionId !== undefined && (typeof value.versionId !== "string" || !/^[0-9a-f-]{36}$/i.test(value.versionId))) ) throw new Error("Invalid Site reference"); return ( ); } catch { return

{labels.siteReferenceInvalid}

; } } function InlineHtml({ content, toolBridge, theme }: ChatInteractiveBlockProps) { const labels = useArtifactLabels(); const html = useMemo(() => inlineHtmlDocument(content), [content]); const bridge = useMemo(() => toolBridge?.(), [toolBridge]); return ( ); } function SiteEmbed( props: ChatInteractiveBlockProps & { siteId: string; initialVersionId?: string }, ) { const [versionId, setVersionId] = useState(props.initialVersionId); return ( ); } function SiteEmbedContent({ workspaceId, client, toolBridge, theme, siteId, versionId, onVersionChange, }: ChatInteractiveBlockProps & { siteId: string; versionId?: string | undefined; onVersionChange: (id: string) => void; }) { const [loaded, setLoaded] = useState<{ client: SiteSnapshotClient; snapshot: Awaited>; } | null>(null); const snapshot = loaded?.client === client ? loaded.snapshot : null; const [error, setError] = useState(false); const [retry, setRetry] = useState(0); useEffect(() => { const abort = new AbortController(); setError(false); setLoaded(null); void loadSiteSnapshot(client, workspaceId, siteId, { signal: abort.signal, ...(versionId ? { versionId } : {}), }) .then((value) => { if (!abort.signal.aborted) setLoaded({ client, snapshot: value }); }) .catch(() => { if (!abort.signal.aborted) setError(true); }); return () => abort.abort(); }, [client, workspaceId, siteId, versionId, retry]); const content = snapshot?.content; const bridge = useMemo( () => content ? toolBridge?.({ artifactId: siteId, siteVersionId: content.versionId, requestedTools: content.requestedTools, }) : undefined, [toolBridge, siteId, content], ); const open = useOpenGeniLinkResolver()?.({ kind: "site", artifactId: siteId, workspaceId }); const labels = useArtifactLabels(); if (error) return (

{labels.siteLoadFailed}{" "} setRetry((v) => v + 1)}> {labels.retry}

); if (!snapshot) return

{labels.loadingSite}

; if (!content) return

{labels.siteUnpublished}

; return ( {/* One published version has nothing to choose; keep the bar for the title. */} {snapshot.detail.versions.length > 1 || !snapshot.detail.versions.some((v) => v.id === content.versionId) ? ( onVersionChange(e.target.value)} > {!snapshot.detail.versions.some((v) => v.id === content.versionId) && ( )} {snapshot.detail.versions.map((v) => ( ))} ) : null} {open?.href ? ( {labels.openSite} ) : open?.open ? ( ) : null} } /> ); }