import { MenuItem } from "@noya-app/noya-designsystem"; import { MediaMap } from "@noya-app/noya-schemas"; import { AIToolInvocation, CallableAIToolsMap } from "../AIManager"; import { EvalRequest, EvalResponse } from "../EvalManager"; import { ClientToServerMessage, ServerToClientMessage } from "../multiplayer"; import { SerializableRequest } from "../rpc/types"; import { RPCResponseMessage } from "../rpcManager"; import { ConnectionMessage } from "./types"; export type AvoidRect = { id: string; rect: { x: number; y: number; width: number; height: number }; }; export type RPCStreamChunkMessage = { id: string; chunk: string; }; export type RPCErrorMessage = { id: string; error: string; }; /** * Messages sent from the embedded editor to the parent frame */ export type EmbeddedToParentMessage = | { type: "multiplayer.ready" } | { type: "multiplayer.message"; payload: ClientToServerMessage } | { type: "application.setMenuItems"; payload: { menuItems: MenuItem[]; position: "left" | "right" }; } | { type: "application.showCommandPalette"; payload: boolean; } | { type: "application.evaluate"; payload: EvalRequest; } | { type: "application.enablePublishing"; payload: boolean; } | { type: "application.publish"; payload: { id: string; files: MediaMap }; } | { type: "ai.setConfig"; payload: { tools: CallableAIToolsMap; systemMessage?: string; }; } | { type: "ai.tool.response"; payload: { invocation: AIToolInvocation; response?: string }; } | { type: "rpc.request"; payload: SerializableRequest }; /** * Messages sent from the parent frame to the embedded editor */ export type ParentToEmbeddedMessage = | ConnectionMessage | RPCResponseMessage | ServerToClientMessage | { type: "application.selectMenuItem"; payload: { value: string } } | { type: "application.evaluate.result"; payload: EvalResponse; } | { type: "application.requestPublish"; payload: { id: string }; } | { type: "ai.tool.call"; payload: AIToolInvocation; } | { type: "application.setAvoidRects"; payload: { rects: AvoidRect[]; }; }; /** * Interface for handling communication between parent frame and embedded editor */ export interface ParentFrameMessageHandler { /** * Send a message to the parent frame * @param message The message to send * @param origin The target origin */ postMessage: ( message: EmbeddedToParentMessage | ParentToEmbeddedMessage, origin: string ) => void; /** * Add a listener for messages from the parent frame * @param handler The message handler function * @returns A cleanup function to remove the listener */ addMessageListener: ( handler: (event: MessageEvent>) => void ) => () => void; } /** * Default implementation of ParentFrameMessageHandler using window.parent */ export function createDefaultMessageHandler(): ParentFrameMessageHandler { return { postMessage: (message, origin) => window.parent.postMessage(message, origin), addMessageListener: (handler) => { window.addEventListener("message", handler); return () => window.removeEventListener("message", handler); }, }; } /** * Type guard to check if a message is a valid ParentToEmbeddedMessage */ export function isParentToEmbeddedMessage( message: any ): message is ParentToEmbeddedMessage { if (!message || typeof message !== "object") { return false; } if (!("type" in message) || typeof message.type !== "string") { return false; } switch (message.type) { case "connection": return ( "state" in message && (message.state === "connected" || message.state === "disconnected") ); case "application.selectMenuItem": return "payload" in message && typeof message.payload === "object"; case "rpc.response": case "rpc.chunk": case "rpc.error": return "payload" in message && typeof message.payload === "object"; default: // For other message types, just check if it's a valid ServerToClientMessage return true; } } /** * Type guard to check if a message is a valid EmbeddedToParentMessage */ export function isEmbeddedToParentMessage( message: any ): message is EmbeddedToParentMessage { if (!message || typeof message !== "object" || !("type" in message)) { return false; } switch (message.type) { case "multiplayer.ready": return true; case "multiplayer.message": return "payload" in message; case "application.setMenuItems": return ( "payload" in message && Array.isArray((message as any).payload?.menuItems) && typeof message.payload.position === "string" && (message.payload.position === "left" || message.payload.position === "right") ); case "rpc.request": return "payload" in message && typeof message.payload === "object"; default: return false; } }