import { Trans, useLingui } from "@lingui/react/macro"; import type { MessageBlock } from "@rakazo/contracts"; import { abortableDelay } from "@rakazo/core"; import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web"; import { X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { BuiCard, SuccessPop } from "../../components/ai/primitives"; import { type ArtifactTarget, decodeArtifactBase64 } from "../../lib/artifact-open"; import { chartViewport } from "../../lib/chart-viewport"; import { connectMcpOauth } from "../../lib/mcp-connect"; import { rpc } from "../../lib/rpc"; export function ChoiceCard({ botId, block, onBotChanged, }: { botId: string; block: Extract; onBotChanged: () => Promise; }) { const { t } = useLingui(); const [pending, setPending] = useState(false); const [error, setError] = useState(null); const [locallyDismissed, setLocallyDismissed] = useState(false); const dismissed = locallyDismissed || block.answerId === "_dismissed"; async function choose(optionId: string) { setPending(true); setError(null); try { await rpc.onboarding.choose({ botId, optionId }); await onBotChanged(); } catch (err) { setError(err instanceof Error ? err.message : t`Could not save this choice`); setPending(false); } } async function dismiss() { setPending(true); setError(null); try { await rpc.onboarding.dismissFocus({ botId }); setLocallyDismissed(true); void onBotChanged().catch(() => undefined); } catch (err) { setError(err instanceof Error ? err.message : t`Could not dismiss`); setPending(false); } } if (dismissed) return null; return (
{!block.answerId ? ( ) : null}
{block.question}
{block.subtitle ? (
{block.subtitle}
) : null}
{block.options .filter((option) => !block.answerId || option.id === block.answerId) .map((option) => ( ))}
{error ?

{error}

: null}
); } export function AppConnectCard({ botId, block, }: { botId: string; block: Extract; }) { const { t } = useLingui(); const [busy, setBusy] = useState(false); const [localStatus, setLocalStatus] = useState<"pending" | "connected">(block.status); const [error, setError] = useState(null); const connectionAttempt = useRef(null); const status = block.status === "connected" ? "connected" : localStatus; useEffect(() => () => connectionAttempt.current?.abort(), []); async function authorize() { connectionAttempt.current?.abort(); const controller = new AbortController(); connectionAttempt.current = controller; setBusy(true); setError(null); try { const started = await rpc.connections.begin({ connectorId: block.connectorId, provider: block.provider, displayName: block.name, }); if (started.authorizationUrl) { window.open(started.authorizationUrl, "rakazo-app-connect", "popup,width=560,height=720"); } for (let i = 0; i < 60; i += 1) { if (controller.signal.aborted) return; const row = await rpc.connections .complete({ connectionId: started.connectionId }) .catch(() => undefined); if (row?.status === "connected") { if (controller.signal.aborted) return; setLocalStatus("connected"); await rpc.onboarding .appConnected({ botId, provider: block.provider, connectorId: block.connectorId }) .catch(() => undefined); return; } await abortableDelay(2_000, controller.signal); } if (!controller.signal.aborted) setError(t`Authorization timed out. Please try again.`); } catch (error) { if (!controller.signal.aborted) { setError(error instanceof Error ? error.message : t`Could not authorize this app`); } } finally { if (connectionAttempt.current === controller) { connectionAttempt.current = null; setBusy(false); } } } return (
{block.logo ? ( ) : ( {block.name.slice(0, 1).toUpperCase()} )} {block.name} {block.description} {status === "connected" ? ( ) : ( )}
{error ?

{error}

: null}
); } function ChartCanvas({ spec, data, width, height, }: { spec: Record; data: unknown[]; width: number; height?: number; }) { const { t } = useLingui(); const ref = useRef(null); const [error, setError] = useState(null); const [meta, setMeta] = useState<{ title?: string; swatches: { label: string; color: string }[]; }>({ swatches: [] }); useEffect(() => { let cancelled = false; // Plot loads lazily so threads without charts never pay for the library. void (async () => { try { const { buildPlotParts } = await import("@rakazo/core/plot"); if (cancelled || !ref.current) return; // Hover inspection by default: give the first mark a tooltip unless // the spec already asks for one somewhere. const marks = Array.isArray((spec as { marks?: unknown[] }).marks) ? ((spec as { marks: { options?: Record }[] }).marks ?? []) : []; const hasTip = marks.some((mark) => mark.options && "tip" in mark.options); const liveSpec = hasTip ? spec : { ...spec, marks: marks.map((mark, index) => index === 0 ? { ...mark, options: { ...(mark.options ?? {}), tip: true } } : mark, ), }; const parts = buildPlotParts(liveSpec as never, data, document, { width, height }); setMeta({ title: parts.title, swatches: parts.swatches }); setError(null); ref.current.replaceChildren(parts.plotted); } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : t`Could not render chart`); } })(); return () => { cancelled = true; }; }, [spec, data, width, height, t]); if (error) return (
Chart failed to render: {error}
); return (
{meta.title ? (
{meta.title}
) : null} {meta.swatches.length > 0 ? (
{meta.swatches.map((swatch) => ( {/* Series colours are data-driven and intentionally distinct. */} {swatch.label} ))}
) : null}
); } type McpApprovalState = "pending" | "connecting" | "connected" | "dismissed"; /** Approval card for an agent-created MCP server: the user completes browser * OAuth (or confirms no authorization is needed) without leaving the chat. */ export function McpApprovalCard({ botId, name, serverId, transport, endpoint, needsOAuth, }: { botId: string | undefined; name: string; serverId: string; transport: string; endpoint: string | null; needsOAuth: boolean; }) { const { t } = useLingui(); const [state, setState] = useState("pending"); const [error, setError] = useState(null); async function authorize() { if (!botId) { setError(t`This server cannot be assigned without a bot.`); return; } setState("connecting"); setError(null); try { if (needsOAuth) { const result = await connectMcpOauth(serverId); if (result === "cancelled") { setState("pending"); return; } } await rpc.mcp.assignments.approve({ botId, serverId }); setState("connected"); } catch (err) { setError(err instanceof Error ? err.message : t`Could not approve this server`); setState("pending"); } } const summary = endpoint ?? `stdio · ${transport}`; return (
M Connect MCP server “{name}”

{summary}

{state === "pending" || state === "connecting" ? ( <>

{needsOAuth ? t`Authorize this server so agents can use its tools. A popup opens.` : t`Approve this server to let your agent use its tools.`}

{error ?

{error}

: null}
) : null} {state === "connected" ? (
) : null} {state === "dismissed" ? (

Dismissed. Reconnect anytime from MCP settings.

) : null}
); } export function ChartBlockView({ name, spec, data, }: { name: string; spec: Record; data: unknown[]; }) { const { t } = useLingui(); const [expanded, setExpanded] = useState(false); const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight, })); useEffect(() => { if (!expanded) return; setViewport({ width: window.innerWidth, height: window.innerHeight }); const onResize = () => setViewport({ width: window.innerWidth, height: window.innerHeight }); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, [expanded]); const expandedViewport = chartViewport(viewport.width, viewport.height); return ( <>
{name} } >
); } export function ArtifactImage({ target, artifactId, name, }: { target: ArtifactTarget; artifactId: string; name: string; }) { const { t } = useLingui(); const [src, setSrc] = useState(null); const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false); const container = useRef(null); const targetBotId = "botId" in target ? target.botId : undefined; const targetGroupId = "groupId" in target ? target.groupId : undefined; useEffect(() => { const element = container.current; if (!element || typeof IntersectionObserver === "undefined") { setVisible(true); return; } const observer = new IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting)) { setVisible(true); observer.disconnect(); } }, { rootMargin: "320px" }, ); observer.observe(element); return () => observer.disconnect(); }, []); useEffect(() => { if (!visible) return; let cancelled = false; let objectUrl: string | null = null; setSrc(null); void rpc.artifacts .get( targetBotId ? { botId: targetBotId, artifactId } : { groupId: targetGroupId ?? "", artifactId }, ) .then((artifact) => { const bytes = decodeArtifactBase64(artifact.contentBase64); objectUrl = URL.createObjectURL( new Blob([new Uint8Array(bytes)], { type: artifact.mimeType }), ); if (cancelled) URL.revokeObjectURL(objectUrl); else setSrc(objectUrl); }) .catch(() => undefined); return () => { cancelled = true; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [artifactId, targetBotId, targetGroupId, visible]); return (
{src ? ( ) : (
{name}
)} {src ? ( {name} } > {name} ) : null}
); }