import type { ThreadMessageLike } from "@assistant-ui/react"; import { z } from "zod"; import type { MessageRecord } from "./thread-persistence"; /** * Zod schema for persisted attachment info */ const PersistedAttachmentSchema = z.object({ id: z.string(), name: z.string(), type: z.string(), contentType: z.string().optional(), imageDataUrl: z.string().optional(), extractedContent: z.string().optional(), }); const AttachmentsPartSchema = z.object({ type: z.literal("attachments"), items: z.array(PersistedAttachmentSchema), }); type PersistedAttachment = z.infer; /** * Extract persisted attachments from message content (type-safe with Zod) */ function extractPersistedAttachments(content: unknown): PersistedAttachment[] { if (!Array.isArray(content)) return []; for (const part of content) { const result = AttachmentsPartSchema.safeParse(part); if (result.success) { return result.data.items; } } return []; } /** * Convert backend message to assistant-ui ThreadMessageLike format * Filters out 'thinking-steps' part as it's handled separately via messageThinkingSteps * Restores attachments for user messages from persisted data */ export function convertToThreadMessage(msg: MessageRecord): ThreadMessageLike { let content: ThreadMessageLike["content"]; if (typeof msg.content === "string") { content = [{ type: "text", text: msg.content }]; } else if (Array.isArray(msg.content)) { // Filter out custom metadata parts - they're handled separately const filteredContent = msg.content.filter((part: unknown) => { if (typeof part !== "object" || part === null || !("type" in part)) return true; const partType = (part as { type: string }).type; // Filter out thinking-steps, mentioned-documents, and attachments return ( partType !== "thinking-steps" && partType !== "mentioned-documents" && partType !== "attachments" ); }); content = filteredContent.length > 0 ? (filteredContent as ThreadMessageLike["content"]) : [{ type: "text", text: "" }]; } else { content = [{ type: "text", text: String(msg.content) }]; } // Restore attachments for user messages let attachments: ThreadMessageLike["attachments"]; if (msg.role === "user") { const persistedAttachments = extractPersistedAttachments(msg.content); if (persistedAttachments.length > 0) { attachments = persistedAttachments.map((att) => ({ id: att.id, name: att.name, type: att.type as "document" | "image" | "file", contentType: att.contentType || "application/octet-stream", status: { type: "complete" as const }, content: [], // Custom fields for our ChatAttachment interface imageDataUrl: att.imageDataUrl, extractedContent: att.extractedContent, })); } } // Build metadata.custom for author display in shared chats const metadata = msg.author_id ? { custom: { author: { displayName: msg.author_display_name ?? null, avatarUrl: msg.author_avatar_url ?? null, }, }, } : undefined; return { id: `msg-${msg.id}`, role: msg.role, content, createdAt: new Date(msg.created_at), attachments, metadata, }; }