import { TLRecord, TLStoreWithStatus, createTLStore, TLStoreSnapshot, } from "@tldraw/tldraw" import { createTLSchema, defaultBindingSchemas, defaultShapeSchemas } from "@tldraw/tlschema" import { useEffect, useState } from "react" import { DocHandle, DocHandleChangePayload } from "@automerge/automerge-repo" import { useLocalAwareness, useRemoteAwareness, } from "@automerge/automerge-repo-react-hooks" import { applyAutomergePatchesToTLStore } from "./AutomergeToTLStore.js" import { applyTLStoreChangesToAutomerge } from "./TLStoreToAutomerge.js" // Import custom shape utilities import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil" import { VideoChatShape } from "@/shapes/VideoChatShapeUtil" import { EmbedShape } from "@/shapes/EmbedShapeUtil" import { MarkdownShape } from "@/shapes/MarkdownShapeUtil" import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil" import { SlideShape } from "@/shapes/SlideShapeUtil" import { PromptShape } from "@/shapes/PromptShapeUtil" import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil" import { TranscriptionShape } from "@/shapes/TranscriptionShapeUtil" import { ObsNoteShape } from "@/shapes/ObsNoteShapeUtil" import { FathomTranscriptShape } from "@/shapes/FathomTranscriptShapeUtil" import { HolonShape } from "@/shapes/HolonShapeUtil" import { ObsidianBrowserShape } from "@/shapes/ObsidianBrowserShapeUtil" import { FathomMeetingsBrowserShape } from "@/shapes/FathomMeetingsBrowserShapeUtil" import { LocationShareShape } from "@/shapes/LocationShareShapeUtil" export function useAutomergeStoreV2({ handle, userId: _userId, }: { handle: DocHandle userId: string }): TLStoreWithStatus { console.log("useAutomergeStoreV2 called with handle:", !!handle) // Create a custom schema that includes all the custom shapes const customSchema = createTLSchema({ shapes: { ...defaultShapeSchemas, ChatBox: {} as any, VideoChat: {} as any, Embed: {} as any, Markdown: {} as any, MycrozineTemplate: {} as any, Slide: {} as any, Prompt: {} as any, SharedPiano: {} as any, Transcription: {} as any, ObsNote: {} as any, FathomTranscript: {} as any, Holon: {} as any, ObsidianBrowser: {} as any, FathomMeetingsBrowser: {} as any, LocationShare: {} as any, }, bindings: defaultBindingSchemas, }) const [store] = useState(() => { const store = createTLStore({ schema: customSchema, shapeUtils: [ ChatBoxShape, VideoChatShape, EmbedShape, MarkdownShape, MycrozineTemplateShape, SlideShape, PromptShape, SharedPianoShape, TranscriptionShape, ObsNoteShape, FathomTranscriptShape, HolonShape, ObsidianBrowserShape, FathomMeetingsBrowserShape, LocationShareShape, ], }) return store }) const [storeWithStatus, setStoreWithStatus] = useState({ status: "loading", }) // Debug: Log store status when it changes useEffect(() => { if (storeWithStatus.status === "synced-remote" && storeWithStatus.store) { const allRecords = storeWithStatus.store.allRecords() const shapes = allRecords.filter(r => r.typeName === 'shape') const pages = allRecords.filter(r => r.typeName === 'page') console.log(`📊 useAutomergeStoreV2: Store synced with ${allRecords.length} total records, ${shapes.length} shapes, ${pages.length} pages`) } }, [storeWithStatus.status, storeWithStatus.store]) /* -------------------- TLDraw <--> Automerge -------------------- */ useEffect(() => { // Early return if handle is not available if (!handle) { setStoreWithStatus({ status: "loading" }) return } const unsubs: (() => void)[] = [] // A hacky workaround to prevent local changes from being applied twice // once into the automerge doc and then back again. let isLocalChange = false // Listen for changes from Automerge and apply them to TLDraw const automergeChangeHandler = (payload: DocHandleChangePayload) => { if (isLocalChange) { isLocalChange = false return } try { // Apply patches from Automerge to TLDraw store if (payload.patches && payload.patches.length > 0) { try { applyAutomergePatchesToTLStore(payload.patches, store) // Only log if there are many patches or if debugging is needed if (payload.patches.length > 5) { console.log(`✅ Successfully applied ${payload.patches.length} patches`) } } catch (patchError) { console.error("Error applying patches, attempting individual patch application:", patchError) // Try applying patches one by one to identify problematic ones let successCount = 0 for (const patch of payload.patches) { try { applyAutomergePatchesToTLStore([patch], store) successCount++ } catch (individualPatchError) { console.error(`Failed to apply individual patch:`, individualPatchError) // Log the problematic patch for debugging console.error("Problematic patch details:", { action: patch.action, path: patch.path, value: 'value' in patch ? patch.value : undefined, patchId: patch.path[1], errorMessage: individualPatchError instanceof Error ? individualPatchError.message : String(individualPatchError) }) // Try to get more context about the failing record const recordId = patch.path[1] as string try { const existingRecord = store.get(recordId as any) console.error("Existing record that failed:", existingRecord) } catch (e) { console.error("Could not retrieve existing record:", e) } } } // Only log if there are failures or many patches if (successCount < payload.patches.length || payload.patches.length > 5) { console.log(`Successfully applied ${successCount} out of ${payload.patches.length} patches`) } } } setStoreWithStatus({ store, status: "synced-remote", connectionStatus: "online", }) } catch (error) { console.error("Error applying Automerge patches to TLDraw:", error) setStoreWithStatus({ store, status: "synced-remote", connectionStatus: "offline", error: error instanceof Error ? error : new Error("Unknown error") as any, }) } } handle.on("change", automergeChangeHandler) // Listen for changes from TLDraw and apply them to Automerge // CRITICAL: Listen to ALL sources, not just "user", to catch richText/text changes const unsubscribeTLDraw = store.listen(({ changes, source }) => { // DEBUG: Log all changes to see what's being detected const totalChanges = Object.keys(changes.added || {}).length + Object.keys(changes.updated || {}).length + Object.keys(changes.removed || {}).length if (totalChanges > 0) { console.log(`🔍 TLDraw store changes detected (source: ${source}):`, { added: Object.keys(changes.added || {}).length, updated: Object.keys(changes.updated || {}).length, removed: Object.keys(changes.removed || {}).length, source: source }) // DEBUG: Check for richText/text changes in updated records if (changes.updated) { Object.values(changes.updated).forEach(([_, record]) => { if (record.typeName === 'shape') { if (record.type === 'geo' && (record.props as any)?.richText) { console.log(`🔍 Geo shape ${record.id} richText change detected:`, { hasRichText: !!(record.props as any).richText, richTextType: typeof (record.props as any).richText, source: source }) } if (record.type === 'note' && (record.props as any)?.richText) { console.log(`🔍 Note shape ${record.id} richText change detected:`, { hasRichText: !!(record.props as any).richText, richTextType: typeof (record.props as any).richText, richTextContentLength: Array.isArray((record.props as any).richText?.content) ? (record.props as any).richText.content.length : 'not array', source: source }) } if (record.type === 'arrow' && (record.props as any)?.text !== undefined) { console.log(`🔍 Arrow shape ${record.id} text change detected:`, { hasText: !!(record.props as any).text, textValue: (record.props as any).text, source: source }) } if (record.type === 'text' && (record.props as any)?.richText) { console.log(`🔍 Text shape ${record.id} richText change detected:`, { hasRichText: !!(record.props as any).richText, richTextType: typeof (record.props as any).richText, source: source }) } } }) } // DEBUG: Log added shapes to track what's being created if (changes.added) { Object.values(changes.added).forEach((record) => { if (record.typeName === 'shape') { console.log(`🔍 Shape added: ${record.type} (${record.id})`, { type: record.type, id: record.id, hasRichText: !!(record.props as any)?.richText, hasText: !!(record.props as any)?.text, source: source }) } }) } } // CRITICAL: Don't skip changes - always save them to ensure consistency // The isLocalChange flag is only used to prevent feedback loops from Automerge changes // We should always save TLDraw changes, even if they came from Automerge sync // This ensures that all shapes (notes, rectangles, etc.) are consistently persisted try { // Set flag to prevent feedback loop when this change comes back from Automerge isLocalChange = true handle.change((doc) => { applyTLStoreChangesToAutomerge(doc, changes) }) // Reset flag after a short delay to allow Automerge change handler to process // This prevents feedback loops while ensuring all changes are saved setTimeout(() => { isLocalChange = false }, 100) // Only log if there are many changes or if debugging is needed if (totalChanges > 3) { console.log(`✅ Applied ${totalChanges} TLDraw changes to Automerge document`) } else if (totalChanges > 0) { console.log(`✅ Applied ${totalChanges} TLDraw change(s) to Automerge document`) } // Check if the document actually changed const docAfter = handle.doc() } catch (error) { console.error("Error applying TLDraw changes to Automerge:", error) // Reset flag on error to prevent getting stuck isLocalChange = false } }, { // CRITICAL: Don't filter by source - listen to ALL changes // This ensures we catch richText/text changes regardless of their source // (TLDraw might emit these changes with a different source than "user") scope: "document", }) unsubs.push( () => handle.off("change", automergeChangeHandler), unsubscribeTLDraw ) // Initial load - populate TLDraw store from Automerge document const initializeStore = async () => { try { // Only log if debugging is needed // console.log("Starting TLDraw store initialization...") await handle.whenReady() // console.log("Automerge handle is ready") const doc = handle.doc() // Only log if debugging is needed // console.log("Got Automerge document (FIXED VERSION):", { // hasStore: !!doc.store, // storeKeys: doc.store ? Object.keys(doc.store).length : 0, // }) // Skip pre-sanitization to avoid Automerge reference errors // We'll handle validation issues in the record processing loop instead // Force cache refresh - pre-sanitization code has been removed // Initialize store with existing records from Automerge if (doc.store) { const allStoreValues = Object.values(doc.store) console.log("All store values from Automerge:", allStoreValues.map((v: any) => ({ hasTypeName: !!v?.typeName, hasId: !!v?.id, typeName: v?.typeName, id: v?.id }))) // Simple filtering - only keep valid TLDraw records // Skip custom record types like obsidian_vault - they're not TLDraw records // Components should read them directly from Automerge (like ObsidianVaultBrowser does) const records = allStoreValues.filter((record: any) => { if (!record || !record.typeName || !record.id) return false // Skip obsidian_vault records - they're not TLDraw records if (record.typeName === 'obsidian_vault' || (typeof record.id === 'string' && record.id.startsWith('obsidian_vault:'))) { return false } return true }) // Only log if there are many records or if debugging is needed if (records.length > 50) { console.log(`Found ${records.length} valid records in Automerge document`) } // CRITICAL FIXES ONLY - preserve all other properties // Note: obsidian_vault records are filtered out above - they're not TLDraw records const processedRecords = records.map((record: any) => { // Create a deep copy to avoid modifying immutable Automerge objects const processedRecord = JSON.parse(JSON.stringify(record)) // CRITICAL FIXES ONLY - preserve all other properties if (processedRecord.typeName === 'shape') { // Ensure basic required properties exist if (typeof processedRecord.x !== 'number') processedRecord.x = 0 if (typeof processedRecord.y !== 'number') processedRecord.y = 0 if (typeof processedRecord.rotation !== 'number') processedRecord.rotation = 0 if (typeof processedRecord.isLocked !== 'boolean') processedRecord.isLocked = false if (typeof processedRecord.opacity !== 'number') processedRecord.opacity = 1 if (!processedRecord.meta || typeof processedRecord.meta !== 'object') processedRecord.meta = {} if (!processedRecord.index) processedRecord.index = 'a1' if (!processedRecord.parentId) { const pageRecord = records.find((r: any) => r.typeName === 'page') as any if (pageRecord && pageRecord.id) { processedRecord.parentId = pageRecord.id } else { processedRecord.parentId = 'page:page' } } if (!processedRecord.props || typeof processedRecord.props !== 'object') processedRecord.props = {} // CRITICAL: Infer type from properties BEFORE defaulting to 'geo' // This ensures arrows and other shapes are properly recognized if (!processedRecord.type || typeof processedRecord.type !== 'string') { // Check for arrow-specific properties first if (processedRecord.props?.start !== undefined || processedRecord.props?.end !== undefined || processedRecord.props?.arrowheadStart !== undefined || processedRecord.props?.arrowheadEnd !== undefined || processedRecord.props?.kind === 'line' || processedRecord.props?.kind === 'curved' || processedRecord.props?.kind === 'straight') { processedRecord.type = 'arrow' } // Check for line-specific properties else if (processedRecord.props?.points !== undefined) { processedRecord.type = 'line' } // Check for geo-specific properties (w/h/geo) else if (processedRecord.props?.geo !== undefined || ('w' in processedRecord && 'h' in processedRecord) || ('w' in processedRecord.props && 'h' in processedRecord.props)) { processedRecord.type = 'geo' } // Check for note-specific properties else if (processedRecord.props?.growY !== undefined || processedRecord.props?.verticalAlign !== undefined) { processedRecord.type = 'note' } // Check for text-specific properties else if (processedRecord.props?.textAlign !== undefined || processedRecord.props?.autoSize !== undefined) { processedRecord.type = 'text' } // Check for draw-specific properties else if (processedRecord.props?.segments !== undefined) { processedRecord.type = 'draw' } // Default to geo only if no other indicators found else { processedRecord.type = 'geo' } } // CRITICAL: For geo shapes, move w/h/geo from top-level to props (required by TLDraw schema) if (processedRecord.type === 'geo' || ('w' in processedRecord && 'h' in processedRecord && processedRecord.type !== 'arrow')) { if (!processedRecord.type || processedRecord.type === 'geo') { processedRecord.type = 'geo' } // Move w from top-level to props if ('w' in processedRecord && processedRecord.w !== undefined) { if (!('w' in processedRecord.props) || processedRecord.props.w === undefined) { processedRecord.props.w = processedRecord.w } delete (processedRecord as any).w } // Move h from top-level to props if ('h' in processedRecord && processedRecord.h !== undefined) { if (!('h' in processedRecord.props) || processedRecord.props.h === undefined) { processedRecord.props.h = processedRecord.h } delete (processedRecord as any).h } // Move geo from top-level to props if ('geo' in processedRecord && processedRecord.geo !== undefined) { if (!('geo' in processedRecord.props) || processedRecord.props.geo === undefined) { processedRecord.props.geo = processedRecord.geo } delete (processedRecord as any).geo } // Fix richText structure if it exists (preserve content) if (processedRecord.props.richText) { if (Array.isArray(processedRecord.props.richText)) { processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } } else if (typeof processedRecord.props.richText === 'object' && processedRecord.props.richText !== null) { if (!processedRecord.props.richText.type) { processedRecord.props.richText = { ...processedRecord.props.richText, type: 'doc' } } if (!processedRecord.props.richText.content) { processedRecord.props.richText = { ...processedRecord.props.richText, content: [] } } } } } // CRITICAL: For arrow shapes, preserve text property if (processedRecord.type === 'arrow') { if ((processedRecord.props as any).text === undefined || (processedRecord.props as any).text === null) { (processedRecord.props as any).text = '' } } // CRITICAL: For line shapes, ensure points structure exists (required by schema) if (processedRecord.type === 'line') { if ('w' in processedRecord.props) delete (processedRecord.props as any).w if ('h' in processedRecord.props) delete (processedRecord.props as any).h if (!processedRecord.props.points || typeof processedRecord.props.points !== 'object' || Array.isArray(processedRecord.props.points)) { processedRecord.props.points = { 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } } } } // CRITICAL: For group shapes, remove w/h from props (they cause validation errors) if (processedRecord.type === 'group') { if ('w' in processedRecord.props) delete (processedRecord.props as any).w if ('h' in processedRecord.props) delete (processedRecord.props as any).h } // CRITICAL: For image/video shapes, fix crop structure if it exists if (processedRecord.type === 'image' || processedRecord.type === 'video') { if (processedRecord.props.crop !== null && processedRecord.props.crop !== undefined) { if (!processedRecord.props.crop.topLeft || !processedRecord.props.crop.bottomRight) { if (processedRecord.props.crop.x !== undefined && processedRecord.props.crop.y !== undefined) { processedRecord.props.crop = { topLeft: { x: processedRecord.props.crop.x || 0, y: processedRecord.props.crop.y || 0 }, bottomRight: { x: (processedRecord.props.crop.x || 0) + (processedRecord.props.crop.w || 1), y: (processedRecord.props.crop.y || 0) + (processedRecord.props.crop.h || 1) } } } else { processedRecord.props.crop = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } } } } } } // CRITICAL: Fix richText structure for note shapes if it exists if (processedRecord.type === 'note' && processedRecord.props.richText) { if (Array.isArray(processedRecord.props.richText)) { processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } } else if (typeof processedRecord.props.richText === 'object' && processedRecord.props.richText !== null) { if (!processedRecord.props.richText.type) { processedRecord.props.richText = { ...processedRecord.props.richText, type: 'doc' } } if (!processedRecord.props.richText.content) { processedRecord.props.richText = { ...processedRecord.props.richText, content: [] } } } } // Ensure props object exists for all shapes if (!processedRecord.props) processedRecord.props = {} // Preserve original data structure - only move properties when TLDraw validation requires it // Arrow shapes don't have w/h properties, so remove them if present if (processedRecord.type === 'arrow') { if ('w' in processedRecord) { console.log(`Removing invalid w property from arrow shape ${processedRecord.id}`) delete (processedRecord as any).w } if ('h' in processedRecord) { console.log(`Removing invalid h property from arrow shape ${processedRecord.id}`) delete (processedRecord as any).h } } // For other shapes, preserve the original structure - don't move w/h unless validation fails // Handle arrow shapes specially - ensure they have required properties if (processedRecord.type === 'arrow') { // Ensure required arrow properties exist if (!processedRecord.props.kind) processedRecord.props.kind = 'line' if (!processedRecord.props.labelColor) processedRecord.props.labelColor = 'black' if (!processedRecord.props.color) processedRecord.props.color = 'black' if (!processedRecord.props.fill) processedRecord.props.fill = 'none' if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' if (!processedRecord.props.size) processedRecord.props.size = 'm' if (!processedRecord.props.arrowheadStart) processedRecord.props.arrowheadStart = 'none' if (!processedRecord.props.arrowheadEnd) processedRecord.props.arrowheadEnd = 'arrow' if (!processedRecord.props.font) processedRecord.props.font = 'draw' if (!processedRecord.props.start) processedRecord.props.start = { x: 0, y: 0 } if (!processedRecord.props.end) processedRecord.props.end = { x: 100, y: 0 } if (processedRecord.props.bend === undefined) processedRecord.props.bend = 0 if (!processedRecord.props.text) processedRecord.props.text = '' if (processedRecord.props.labelPosition === undefined) processedRecord.props.labelPosition = 0.5 if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 if (processedRecord.props.elbowMidPoint === undefined) processedRecord.props.elbowMidPoint = 0.5 // Remove any invalid properties const invalidArrowProps = ['w', 'h', 'geo', 'insets', 'scribbles'] invalidArrowProps.forEach(prop => { if (prop in processedRecord.props) { console.log(`Removing invalid prop '${prop}' from arrow shape ${processedRecord.id}`) delete (processedRecord.props as any)[prop] } }) } // Handle note shapes specially - ensure they have required properties if (processedRecord.type === 'note') { // Ensure required note properties exist if (!processedRecord.props.color) processedRecord.props.color = 'black' if (!processedRecord.props.labelColor) processedRecord.props.labelColor = 'black' if (!processedRecord.props.size) processedRecord.props.size = 'm' if (!processedRecord.props.font) processedRecord.props.font = 'draw' if (processedRecord.props.fontSizeAdjustment === undefined) processedRecord.props.fontSizeAdjustment = 0 if (!processedRecord.props.align) processedRecord.props.align = 'start' if (!processedRecord.props.verticalAlign) processedRecord.props.verticalAlign = 'start' if (processedRecord.props.growY === undefined) processedRecord.props.growY = 0 if (!processedRecord.props.url) processedRecord.props.url = '' // Note: richText is not required for note shapes if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 // Remove any invalid properties const invalidNoteProps = ['w', 'h', 'geo', 'insets', 'scribbles'] invalidNoteProps.forEach(prop => { if (prop in processedRecord.props) { console.log(`Removing invalid prop '${prop}' from note shape ${processedRecord.id}`) delete (processedRecord.props as any)[prop] } }) } // Handle text shapes specially - ensure they have required properties if (processedRecord.type === 'text') { // Ensure required text properties exist (matching default tldraw text shape schema) if (!processedRecord.props.color) processedRecord.props.color = 'black' if (!processedRecord.props.size) processedRecord.props.size = 'm' if (!processedRecord.props.font) processedRecord.props.font = 'draw' if (!processedRecord.props.textAlign) processedRecord.props.textAlign = 'start' if (processedRecord.props.w === undefined || processedRecord.props.w === null) { processedRecord.props.w = 100 } if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 if (processedRecord.props.autoSize === undefined) processedRecord.props.autoSize = false // Ensure richText property exists for text shapes if (!processedRecord.props.richText) { console.log(`🔧 Creating default richText object for text shape ${processedRecord.id}`) processedRecord.props.richText = { content: [], type: 'doc' } } // Remove any invalid properties (including 'text' property which is not in default schema) // Note: richText is actually required for text shapes, so don't remove it const invalidTextProps = ['text', 'h', 'geo', 'insets', 'scribbles', 'isMinimized', 'roomUrl', 'roomId', 'align', 'verticalAlign', 'growY', 'url'] invalidTextProps.forEach(prop => { if (prop in processedRecord.props) { console.log(`Removing invalid prop '${prop}' from text shape ${processedRecord.id}`) delete (processedRecord.props as any)[prop] } }) } // Handle draw shapes specially - ensure they have required properties if (processedRecord.type === 'draw') { // Ensure required draw properties exist if (!processedRecord.props.color) processedRecord.props.color = 'black' if (!processedRecord.props.fill) processedRecord.props.fill = 'none' if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' if (!processedRecord.props.size) processedRecord.props.size = 'm' // Validate and fix segments array - this is critical for preventing Polyline2d errors if (!processedRecord.props.segments || !Array.isArray(processedRecord.props.segments)) { console.log(`🔧 Fixing missing/invalid segments for draw shape ${processedRecord.id}`) processedRecord.props.segments = [ { type: "free", points: [ { x: 0, y: 0, z: 0.5 }, { x: 10, y: 10, z: 0.5 } ] } ] } else { // Validate each segment in the array // Polyline2d requires at least 2 points per segment const validSegments = [] for (let i = 0; i < processedRecord.props.segments.length; i++) { const segment = processedRecord.props.segments[i] if (segment && typeof segment === 'object' && segment.type && Array.isArray(segment.points) && segment.points.length >= 2) { // Validate points in the segment const validPoints = segment.points.filter((point: any) => point && typeof point === 'object' && typeof point.x === 'number' && typeof point.y === 'number' && !isNaN(point.x) && !isNaN(point.y) ) // Polyline2d requires at least 2 points if (validPoints.length >= 2) { validSegments.push({ type: segment.type, points: validPoints }) } else if (validPoints.length === 1) { // If only 1 point, duplicate it to create a valid 2-point segment console.log(`🔧 Draw shape ${processedRecord.id} segment ${i} has only 1 point, duplicating to create valid segment`) validSegments.push({ type: segment.type, points: [validPoints[0], { ...validPoints[0] }] }) } } } if (validSegments.length === 0) { console.log(`🔧 All segments invalid for draw shape ${processedRecord.id}, creating default segment`) processedRecord.props.segments = [ { type: "free", points: [ { x: 0, y: 0, z: 0.5 }, { x: 10, y: 10, z: 0.5 } ] } ] } else { processedRecord.props.segments = validSegments } } if (processedRecord.props.isComplete === undefined) processedRecord.props.isComplete = true if (processedRecord.props.isClosed === undefined) processedRecord.props.isClosed = false if (processedRecord.props.isPen === undefined) processedRecord.props.isPen = false if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 // Remove any invalid properties const invalidDrawProps = ['w', 'h', 'geo', 'insets', 'scribbles', 'richText'] invalidDrawProps.forEach(prop => { if (prop in processedRecord.props) { console.log(`Removing invalid prop '${prop}' from draw shape ${processedRecord.id}`) delete (processedRecord.props as any)[prop] } }) } // Handle geo shapes specially - ensure geo property is in props where TLDraw expects it if (processedRecord.type === 'geo') { // Ensure props exists if (!processedRecord.props) processedRecord.props = {} // CRITICAL: ALWAYS remove w/h/geo from top level (TLDraw validation fails if they exist at top level) // Move w from top level to props (preserve value if not already in props) if ('w' in processedRecord) { console.log(`🔧 Geo shape fix: Removing w from top level for shape ${processedRecord.id}`) if (!('w' in processedRecord.props) || processedRecord.props.w === undefined) { processedRecord.props.w = (processedRecord as any).w } delete (processedRecord as any).w } // Move h from top level to props (preserve value if not already in props) if ('h' in processedRecord) { console.log(`🔧 Geo shape fix: Removing h from top level for shape ${processedRecord.id}`) if (!('h' in processedRecord.props) || processedRecord.props.h === undefined) { processedRecord.props.h = (processedRecord as any).h } delete (processedRecord as any).h } // Move geo from top level to props (preserve value if not already in props) if ('geo' in processedRecord) { console.log(`🔧 Geo shape fix: Removing geo from top level for shape ${processedRecord.id}`) if (!('geo' in processedRecord.props) || processedRecord.props.geo === undefined) { processedRecord.props.geo = (processedRecord as any).geo } delete (processedRecord as any).geo } // Ensure geo property exists in props with a default value if (!processedRecord.props.geo) { processedRecord.props.geo = 'rectangle' } // Ensure w/h exist in props with defaults if missing if (!processedRecord.props) processedRecord.props = {} if (processedRecord.props.w === undefined || processedRecord.props.w === null) { processedRecord.props.w = 100 } if (processedRecord.props.h === undefined || processedRecord.props.h === null) { processedRecord.props.h = 100 } if (processedRecord.props.geo === undefined || processedRecord.props.geo === null) { processedRecord.props.geo = 'rectangle' } if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' if (!processedRecord.props.growY) processedRecord.props.growY = 0 if (!processedRecord.props.url) processedRecord.props.url = '' if (!processedRecord.props.scale) processedRecord.props.scale = 1 if (!processedRecord.props.color) processedRecord.props.color = 'black' if (!processedRecord.props.labelColor) processedRecord.props.labelColor = 'black' if (!processedRecord.props.fill) processedRecord.props.fill = 'none' if (!processedRecord.props.size) processedRecord.props.size = 'm' if (!processedRecord.props.font) processedRecord.props.font = 'draw' if (!processedRecord.props.align) processedRecord.props.align = 'middle' if (!processedRecord.props.verticalAlign) processedRecord.props.verticalAlign = 'middle' // Note: richText IS required for geo shapes in TLDraw if (!processedRecord.props.richText) processedRecord.props.richText = { content: [], type: 'doc' } // Ensure basic geo properties exist if (!processedRecord.props.geo) processedRecord.props.geo = 'rectangle' if (!processedRecord.props.fill) processedRecord.props.fill = 'solid' if (!processedRecord.props.color) processedRecord.props.color = 'white' // Validate geo property const validGeoTypes = [ 'cloud', 'rectangle', 'ellipse', 'triangle', 'diamond', 'pentagon', 'hexagon', 'octagon', 'star', 'rhombus', 'rhombus-2', 'oval', 'trapezoid', 'arrow-right', 'arrow-left', 'arrow-up', 'arrow-down', 'x-box', 'check-box', 'heart' ] if (!validGeoTypes.includes(processedRecord.props.geo)) { console.log(`Setting valid geo property for shape ${processedRecord.id} (was: ${processedRecord.props.geo})`) processedRecord.props.geo = 'rectangle' } // Remove invalid properties from props (only log if actually removing) const invalidProps = ['insets', 'scribbles'] invalidProps.forEach(prop => { if (prop in processedRecord.props) { delete (processedRecord.props as any)[prop] } }) } // Handle rich text content that might be undefined or invalid // Only process richText for shapes that actually use it (text, note, geo, etc.) // CRITICAL: geo shapes (rectangles) can legitimately have richText in TLDraw if (processedRecord.type === 'text' || processedRecord.type === 'note' || processedRecord.type === 'geo') { if (processedRecord.props && processedRecord.props.richText !== undefined) { if (!Array.isArray(processedRecord.props.richText) && typeof processedRecord.props.richText !== 'object') { console.warn('Fixing invalid richText property for shape:', processedRecord.id, 'type:', processedRecord.type, 'was:', typeof processedRecord.props.richText) processedRecord.props.richText = { content: [], type: 'doc' } } else if (Array.isArray(processedRecord.props.richText)) { // If it's an array, convert to proper richText object structure console.log(`🔧 Converting richText array to object for shape ${processedRecord.id}`) processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } } } else { // Create default empty richText object for text shapes (but not for geo/note unless they already have it) if (processedRecord.type === 'text') { if (!processedRecord.props) processedRecord.props = {} processedRecord.props.richText = { content: [], type: 'doc' } } } } else if (processedRecord.props && processedRecord.props.richText !== undefined) { // Remove richText from shapes that don't use it (but preserve for geo/note which are handled above) delete (processedRecord.props as any).richText } // Remove invalid properties that cause validation errors (after moving geo properties) const invalidProperties = [ 'insets', 'scribbles', 'duplicateProps', 'isAspectRatioLocked', 'isFlippedHorizontal', 'isFlippedVertical', 'isFrozen', 'isSnappable', 'isTransparent', 'isVisible', 'isZIndexLocked', 'isHidden' ] invalidProperties.forEach(prop => { if (prop in processedRecord) { delete (processedRecord as any)[prop] } }) // Custom shapes are supported natively by our custom schema - no conversion needed! // Just ensure they have the required properties for their type if (processedRecord.type === 'VideoChat' || processedRecord.type === 'ChatBox' || processedRecord.type === 'Embed' || processedRecord.type === 'SharedPiano' || processedRecord.type === 'MycrozineTemplate' || processedRecord.type === 'Slide') { // These are embed-like shapes - ensure they have basic properties if (!processedRecord.props) processedRecord.props = {} if (processedRecord.props.w === undefined || processedRecord.props.w === null) { processedRecord.props.w = 300 } if (processedRecord.props.h === undefined || processedRecord.props.h === null) { processedRecord.props.h = 200 } console.log(`🔧 Ensured embed-like shape ${processedRecord.type} has required properties:`, processedRecord.props) } else if (processedRecord.type === 'Prompt' || processedRecord.type === 'Transcription' || processedRecord.type === 'Markdown') { // These are text-like shapes - ensure they have text properties if (!processedRecord.props) processedRecord.props = {} if (processedRecord.props.w === undefined || processedRecord.props.w === null) { processedRecord.props.w = 300 } // Convert value property to richText if it exists (for Prompt shapes) if (processedRecord.props.value && !processedRecord.props.richText) { processedRecord.props.richText = { content: [ { type: 'paragraph', content: [ { type: 'text', text: processedRecord.props.value } ] } ], type: 'doc' } console.log(`🔧 Converted value to richText for ${processedRecord.type} shape ${processedRecord.id}`) } if (!processedRecord.props.richText) { processedRecord.props.richText = { content: [], type: 'doc' } } console.log(`🔧 Ensured text-like shape ${processedRecord.type} has required properties:`, processedRecord.props) } // Validate that the shape type is supported by our schema const validCustomShapes = ['ObsNote', 'VideoChat', 'Transcription', 'SharedPiano', 'Prompt', 'ChatBox', 'Embed', 'Markdown', 'MycrozineTemplate', 'Slide', 'FathomTranscript', 'Holon', 'ObsidianBrowser', 'HolonBrowser', 'FathomMeetingsBrowser', 'LocationShare'] const validDefaultShapes = ['arrow', 'bookmark', 'draw', 'embed', 'frame', 'geo', 'group', 'highlight', 'image', 'line', 'note', 'text', 'video'] const allValidShapes = [...validCustomShapes, ...validDefaultShapes] if (!allValidShapes.includes(processedRecord.type)) { console.log(`🔧 Unknown shape type ${processedRecord.type}, converting to text shape for shape:`, processedRecord.id) processedRecord.type = 'text' if (!processedRecord.props) processedRecord.props = {} // Preserve existing props and only set defaults for missing required text shape properties // This prevents losing metadata or other valid properties processedRecord.props = { ...processedRecord.props, // Preserve existing props w: processedRecord.props.w || 300, color: processedRecord.props.color || 'black', size: processedRecord.props.size || 'm', font: processedRecord.props.font || 'draw', textAlign: processedRecord.props.textAlign || 'start', autoSize: processedRecord.props.autoSize !== undefined ? processedRecord.props.autoSize : false, scale: processedRecord.props.scale || 1, richText: processedRecord.props.richText || { content: [], type: 'doc' } } // Remove invalid properties for text shapes (but preserve meta and other valid top-level properties) const invalidTextProps = ['h', 'geo', 'insets', 'scribbles', 'isMinimized', 'roomUrl', 'text', 'align', 'verticalAlign', 'growY', 'url'] invalidTextProps.forEach(prop => { if (prop in processedRecord.props) { delete (processedRecord.props as any)[prop] } }) console.log(`🔧 Converted unknown shape to text:`, processedRecord.props) } // Universal shape validation - ensure any shape type can be imported // CRITICAL: Fix image and video shapes FIRST - ensure crop has correct structure // Tldraw expects crop to be { topLeft: { x, y }, bottomRight: { x, y } } or null if (processedRecord.type === 'image' || processedRecord.type === 'video') { // Ensure props exists for image/video shapes if (!processedRecord.props) { processedRecord.props = {} } // Fix crop structure if (processedRecord.props.crop !== null && processedRecord.props.crop !== undefined) { // If crop exists but has wrong structure, fix it if (!processedRecord.props.crop.topLeft || !processedRecord.props.crop.bottomRight) { // Convert old format { x, y, w, h } to new format, or set default if (processedRecord.props.crop.x !== undefined && processedRecord.props.crop.y !== undefined) { // Old format: convert to new format processedRecord.props.crop = { topLeft: { x: processedRecord.props.crop.x || 0, y: processedRecord.props.crop.y || 0 }, bottomRight: { x: (processedRecord.props.crop.x || 0) + (processedRecord.props.crop.w || 1), y: (processedRecord.props.crop.y || 0) + (processedRecord.props.crop.h || 1) } } } else { // Invalid structure: set to default (full crop) processedRecord.props.crop = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } } } } else { // Ensure topLeft and bottomRight are proper objects if (!processedRecord.props.crop.topLeft || typeof processedRecord.props.crop.topLeft !== 'object') { processedRecord.props.crop.topLeft = { x: 0, y: 0 } } if (!processedRecord.props.crop.bottomRight || typeof processedRecord.props.crop.bottomRight !== 'object') { processedRecord.props.crop.bottomRight = { x: 1, y: 1 } } } } else { // Crop is null/undefined: set to null (no crop) processedRecord.props.crop = null } } // CRITICAL: Fix line shapes - ensure valid points and remove invalid w/h properties if (processedRecord.type === 'line') { if (!processedRecord.props) { processedRecord.props = {} } // Line shapes should NOT have w or h properties if ('w' in processedRecord.props) { console.log(`🔧 Universal fix: Removing invalid w property from line shape ${processedRecord.id}`) delete processedRecord.props.w } if ('h' in processedRecord.props) { console.log(`🔧 Universal fix: Removing invalid h property from line shape ${processedRecord.id}`) delete processedRecord.props.h } // Line shapes REQUIRE points property: Record if (!processedRecord.props.points || typeof processedRecord.props.points !== 'object' || Array.isArray(processedRecord.props.points)) { console.log(`🔧 Universal fix: Creating default points for line shape ${processedRecord.id}`) // Create default points with at least 2 points const point1 = { id: 'a1', index: 'a1' as any, x: 0, y: 0 } const point2 = { id: 'a2', index: 'a2' as any, x: 100, y: 0 } processedRecord.props.points = { 'a1': point1, 'a2': point2 } } else { // Validate and fix existing points const validPoints: Record = {} let pointIndex = 0 const indices = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10'] for (const [key, point] of Object.entries(processedRecord.props.points)) { if (point && typeof point === 'object' && typeof (point as any).x === 'number' && typeof (point as any).y === 'number' && !isNaN((point as any).x) && !isNaN((point as any).y)) { const index = indices[pointIndex] || `a${pointIndex + 1}` validPoints[index] = { id: index, index: index as any, x: (point as any).x, y: (point as any).y } pointIndex++ } } if (Object.keys(validPoints).length === 0) { // No valid points, create default console.log(`🔧 Universal fix: No valid points found for line shape ${processedRecord.id}, creating default points`) processedRecord.props.points = { 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } } } else if (Object.keys(validPoints).length === 1) { // Only one point, add a second one const firstPoint = Object.values(validPoints)[0] const secondIndex = indices[1] || 'a2' validPoints[secondIndex] = { id: secondIndex, index: secondIndex as any, x: firstPoint.x + 100, y: firstPoint.y } processedRecord.props.points = validPoints } else { processedRecord.props.points = validPoints } } // Ensure other required line shape properties exist if (!processedRecord.props.color) processedRecord.props.color = 'black' if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' if (!processedRecord.props.size) processedRecord.props.size = 'm' if (!processedRecord.props.spline) processedRecord.props.spline = 'line' if (processedRecord.props.scale === undefined || processedRecord.props.scale === null) { processedRecord.props.scale = 1 } } // CRITICAL: Fix group shapes - remove invalid w/h properties if (processedRecord.type === 'group') { if (!processedRecord.props) { processedRecord.props = {} } // Group shapes should NOT have w or h properties if ('w' in processedRecord.props) { console.log(`🔧 Universal fix: Removing invalid w property from group shape ${processedRecord.id}`) delete processedRecord.props.w } if ('h' in processedRecord.props) { console.log(`🔧 Universal fix: Removing invalid h property from group shape ${processedRecord.id}`) delete processedRecord.props.h } } if (processedRecord.props) { // Fix any richText issues for text shapes only if (processedRecord.type === 'text' && processedRecord.props.richText !== undefined) { if (!Array.isArray(processedRecord.props.richText)) { console.log(`🔧 Universal fix: Converting richText to proper object for text shape ${processedRecord.id}`) processedRecord.props.richText = { content: [], type: 'doc' } } else { // Convert array to proper object structure console.log(`🔧 Universal fix: Converting richText array to object for text shape ${processedRecord.id}`) processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } } } // Special handling for geo shapes if (processedRecord.type === 'geo') { // Geo shapes should have richText property but not text property if ('text' in processedRecord.props) { console.log(`🔧 Removing invalid text property from geo shape ${processedRecord.id}`) delete processedRecord.props.text } // Ensure richText property exists and is properly structured for geo shapes if (!processedRecord.props.richText) { console.log(`🔧 Adding missing richText property for geo shape ${processedRecord.id}`) processedRecord.props.richText = { content: [], type: 'doc' } } else if (Array.isArray(processedRecord.props.richText)) { console.log(`🔧 Converting richText array to object for geo shape ${processedRecord.id}`) processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } } else if (typeof processedRecord.props.richText !== 'object' || processedRecord.props.richText === null) { console.log(`🔧 Fixing invalid richText structure for geo shape ${processedRecord.id}`) processedRecord.props.richText = { content: [], type: 'doc' } } else if (!processedRecord.props.richText.content) { // If richText exists but content is missing, preserve the rest and add empty content console.log(`🔧 Adding missing content to richText for geo shape ${processedRecord.id}`) processedRecord.props.richText = { ...processedRecord.props.richText, content: processedRecord.props.richText.content || [], type: processedRecord.props.richText.type || 'doc' } } // Ensure geo shape has proper structure if (!processedRecord.props.geo) { processedRecord.props.geo = 'rectangle' } if (processedRecord.props.w === undefined || processedRecord.props.w === null) { processedRecord.props.w = 100 } if (processedRecord.props.h === undefined || processedRecord.props.h === null) { processedRecord.props.h = 100 } // Fix dash property - ensure it's a valid value if (processedRecord.props.dash === '' || processedRecord.props.dash === undefined) { processedRecord.props.dash = 'solid' } else if (!['draw', 'solid', 'dashed', 'dotted'].includes(processedRecord.props.dash)) { console.log(`🔧 Fixing invalid dash value '${processedRecord.props.dash}' for geo shape:`, processedRecord.id) processedRecord.props.dash = 'solid' } // Fix scale property - ensure it's a number if (processedRecord.props.scale === undefined || processedRecord.props.scale === null) { processedRecord.props.scale = 1 } else if (typeof processedRecord.props.scale !== 'number') { console.log(`🔧 Fixing invalid scale value '${processedRecord.props.scale}' for geo shape:`, processedRecord.id) processedRecord.props.scale = 1 } // Remove invalid properties for geo shapes (including insets) - but NOT richText as it's required const invalidGeoOtherProps = ['transcript', 'isTranscribing', 'isPaused', 'isEditing', 'roomUrl', 'roomId', 'prompt', 'value', 'agentBinding', 'isMinimized', 'noteId', 'title', 'content', 'tags', 'showPreview', 'backgroundColor', 'textColor', 'editingContent', 'vaultName', 'insets'] invalidGeoOtherProps.forEach(prop => { if (prop in processedRecord.props) { console.log(`🔧 Removing invalid ${prop} property from geo shape:`, processedRecord.id) delete processedRecord.props[prop] } }) } // Fix note shapes - ensure richText exists and remove invalid w/h properties if (processedRecord.type === 'note') { // Note shapes REQUIRE richText property (it's part of the schema) if (!processedRecord.props.richText || typeof processedRecord.props.richText !== 'object') { console.log(`🔧 Adding missing richText property for note shape ${processedRecord.id}`) processedRecord.props.richText = { content: [], type: 'doc' } } if ('w' in processedRecord.props) { console.log(`🔧 Removing invalid w property from note shape:`, processedRecord.id) delete processedRecord.props.w } if ('h' in processedRecord.props) { console.log(`🔧 Removing invalid h property from note shape:`, processedRecord.id) delete processedRecord.props.h } } // Fix text shapes - remove h property if (processedRecord.type === 'text') { if ('h' in processedRecord.props) { console.log(`🔧 Removing invalid h property from text shape:`, processedRecord.id) delete processedRecord.props.h } } // Fix embed shapes - ensure required properties and remove invalid ones if (processedRecord.type === 'embed') { if (!processedRecord.props.url) { console.log(`🔧 Adding missing url property for embed shape:`, processedRecord.id) processedRecord.props.url = '' } if (!processedRecord.props.w) { processedRecord.props.w = 400 } if (!processedRecord.props.h) { processedRecord.props.h = 300 } // Remove invalid properties for embed shapes const invalidEmbedProps = ['isMinimized', 'roomUrl', 'roomId', 'color', 'fill', 'dash', 'size', 'text', 'font', 'align', 'verticalAlign', 'growY', 'richText'] invalidEmbedProps.forEach(prop => { if (prop in processedRecord.props) { console.log(`🔧 Removing invalid prop '${prop}' from embed shape ${processedRecord.id}`) delete (processedRecord.props as any)[prop] } }) } // Ensure all required properties exist for any shape type (except arrow, draw, line, text, note, and group) if (processedRecord.type !== 'arrow' && processedRecord.type !== 'draw' && processedRecord.type !== 'line' && processedRecord.type !== 'text' && processedRecord.type !== 'note' && processedRecord.type !== 'group') { const requiredProps = ['w', 'h'] requiredProps.forEach(prop => { if (processedRecord.props[prop] === undefined) { console.log(`🔧 Universal fix: Adding missing ${prop} for shape ${processedRecord.id} (type: ${processedRecord.type})`) if (prop === 'w' && processedRecord.props.w === undefined) processedRecord.props.w = 100 if (prop === 'h' && processedRecord.props.h === undefined) processedRecord.props.h = 100 } }) } else if (processedRecord.type === 'text') { // Text shapes only need w, not h if (processedRecord.props.w === undefined || processedRecord.props.w === null) { console.log(`🔧 Universal fix: Adding missing w for text shape ${processedRecord.id}`) processedRecord.props.w = 100 } } // Clean up any null/undefined values in props (but preserve required objects like crop for images/videos) // IMPORTANT: crop is already set above for image/video shapes, so we must skip it here Object.keys(processedRecord.props).forEach(propKey => { // Skip crop for image/video shapes - it must be an object, not undefined if ((processedRecord.type === 'image' || processedRecord.type === 'video') && propKey === 'crop') { return // crop is required and already set above } if (processedRecord.props[propKey] === null || processedRecord.props[propKey] === undefined) { console.log(`🔧 Universal fix: Removing null/undefined prop ${propKey} from shape ${processedRecord.id}`) delete processedRecord.props[propKey] } }) } } // Fix instance records if (processedRecord.typeName === 'instance') { if (!processedRecord.meta) processedRecord.meta = {} if ('insets' in processedRecord && !Array.isArray(processedRecord.insets)) { processedRecord.insets = [false, false, false, false] } // Always ensure scribbles is an array, even if undefined if (!Array.isArray(processedRecord.scribbles)) { processedRecord.scribbles = [] } // Always ensure duplicateProps is an object with required properties if (typeof processedRecord.duplicateProps !== 'object' || processedRecord.duplicateProps === null) { processedRecord.duplicateProps = {} } // Ensure duplicateProps has the required shapeIds array if (!Array.isArray(processedRecord.duplicateProps.shapeIds)) { processedRecord.duplicateProps.shapeIds = [] } // Ensure duplicateProps has the required offset object if (typeof processedRecord.duplicateProps.offset !== 'object' || processedRecord.duplicateProps.offset === null) { processedRecord.duplicateProps.offset = { x: 0, y: 0 } } } return processedRecord }) console.log(`Processed ${processedRecords.length} records for loading`) // Debug: Log shape structures before loading const shapesToLoad = processedRecords.filter(r => r.typeName === 'shape') console.log(`📊 About to load ${shapesToLoad.length} shapes into store`) if (shapesToLoad.length > 0) { console.log("📊 Sample processed shape structure:", { id: shapesToLoad[0].id, type: shapesToLoad[0].type, x: shapesToLoad[0].x, y: shapesToLoad[0].y, props: shapesToLoad[0].props, parentId: shapesToLoad[0].parentId, allKeys: Object.keys(shapesToLoad[0]) }) // Log all shapes with their positions console.log("📊 All processed shapes:", shapesToLoad.map(s => ({ id: s.id, type: s.type, x: s.x, y: s.y, hasProps: !!s.props, propsW: s.props?.w, propsH: s.props?.h, parentId: s.parentId }))) } // Load records into store if (processedRecords.length > 0) { console.log("Attempting to load records into store...") // Final validation: ensure all shapes are properly structured processedRecords.forEach(record => { if (record.typeName === 'shape') { // Final check for geo shapes - ALWAYS remove w/h/geo from top level (even if in props) if (record.type === 'geo') { // ALWAYS delete w from top level (TLDraw validation fails if it exists at top level) if ('w' in record) { console.log(`🔧 FINAL PRE-STORE FIX: Removing w from top level for geo shape ${record.id}`) if (!record.props) record.props = {} if (!('w' in record.props) || record.props.w === undefined) { record.props.w = (record as any).w } delete (record as any).w } // ALWAYS delete h from top level if ('h' in record) { console.log(`🔧 FINAL PRE-STORE FIX: Removing h from top level for geo shape ${record.id}`) if (!record.props) record.props = {} if (!('h' in record.props) || record.props.h === undefined) { record.props.h = (record as any).h } delete (record as any).h } // ALWAYS delete geo from top level if ('geo' in record) { console.log(`🔧 FINAL PRE-STORE FIX: Removing geo from top level for geo shape ${record.id}`) if (!record.props) record.props = {} if (!('geo' in record.props) || record.props.geo === undefined) { record.props.geo = (record as any).geo } delete (record as any).geo } } // Ensure text shapes have richText if (record.type === 'text') { if (!record.props) { record.props = {} } if (!record.props.richText) { console.log(`🔧 Final fix: Adding richText to text shape ${record.id}`) record.props.richText = { content: [], type: 'doc' } } } } }) try { store.mergeRemoteChanges(() => { // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level // Note: obsidian_vault records are already filtered out above const sanitizedRecords = processedRecords.map(record => { if (record.typeName === 'shape' && record.type === 'geo') { const sanitized = { ...record } // ALWAYS remove from top level if present if ('w' in sanitized) { console.log(`🔧 LAST-CHANCE FIX: Removing w from top level for geo shape ${sanitized.id}`) if (!sanitized.props) sanitized.props = {} if (!('w' in sanitized.props) || sanitized.props.w === undefined) { sanitized.props.w = (sanitized as any).w } delete (sanitized as any).w } if ('h' in sanitized) { console.log(`🔧 LAST-CHANCE FIX: Removing h from top level for geo shape ${sanitized.id}`) if (!sanitized.props) sanitized.props = {} if (!('h' in sanitized.props) || sanitized.props.h === undefined) { sanitized.props.h = (sanitized as any).h } delete (sanitized as any).h } if ('geo' in sanitized) { console.log(`🔧 LAST-CHANCE FIX: Removing geo from top level for geo shape ${sanitized.id}`) if (!sanitized.props) sanitized.props = {} if (!('geo' in sanitized.props) || sanitized.props.geo === undefined) { sanitized.props.geo = (sanitized as any).geo } delete (sanitized as any).geo } return sanitized } return record }) // Put TLDraw records into store if (sanitizedRecords.length > 0) { store.put(sanitizedRecords) } }) console.log("Successfully loaded all records into store") } catch (error) { console.error("Error loading records into store:", error) // Try loading records one by one to identify problematic ones console.log("Attempting to load records one by one...") let successCount = 0 const failedRecords = [] for (const record of processedRecords) { // Final validation for individual record: ensure text shapes have richText if (record.type === 'text') { if (!record.props) { record.props = {} } if (!record.props.richText) { console.log(`🔧 Individual fix: Adding richText to text shape ${record.id}`) record.props.richText = { content: [], type: 'doc' } } } try { // CRITICAL: Final validation before putting record into store if (record.typeName === 'shape' && record.type === 'geo') { // ALWAYS remove w/h/geo from top level (TLDraw validation fails if they exist at top level) if ('w' in record) { console.log(`🔧 INDIVIDUAL PRE-STORE FIX: Removing w from top level for geo shape ${record.id}`) if (!record.props) record.props = {} if (!('w' in record.props) || record.props.w === undefined) { record.props.w = (record as any).w } delete (record as any).w } if ('h' in record) { console.log(`🔧 INDIVIDUAL PRE-STORE FIX: Removing h from top level for geo shape ${record.id}`) if (!record.props) record.props = {} if (!('h' in record.props) || record.props.h === undefined) { record.props.h = (record as any).h } delete (record as any).h } if ('geo' in record) { console.log(`🔧 INDIVIDUAL PRE-STORE FIX: Removing geo from top level for geo shape ${record.id}`) if (!record.props) record.props = {} if (!('geo' in record.props) || record.props.geo === undefined) { record.props.geo = (record as any).geo } delete (record as any).geo } // Ensure geo property exists in props if (!record.props) record.props = {} if (!record.props.geo) { record.props.geo = 'rectangle' } } // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level let recordToPut = record if (record.typeName === 'shape' && record.type === 'geo') { // Store values before removing from top level const wValue = 'w' in record ? (record as any).w : undefined const hValue = 'h' in record ? (record as any).h : undefined const geoValue = 'geo' in record ? (record as any).geo : undefined // Create cleaned record without w/h/geo at top level const cleaned: any = {} for (const key in record) { if (key !== 'w' && key !== 'h' && key !== 'geo') { cleaned[key] = (record as any)[key] } } // Ensure props exists and move values there if needed if (!cleaned.props) cleaned.props = {} if (wValue !== undefined && (!('w' in cleaned.props) || cleaned.props.w === undefined)) { cleaned.props.w = wValue } if (hValue !== undefined && (!('h' in cleaned.props) || cleaned.props.h === undefined)) { cleaned.props.h = hValue } if (geoValue !== undefined && (!('geo' in cleaned.props) || cleaned.props.geo === undefined)) { cleaned.props.geo = geoValue } recordToPut = cleaned as any } store.mergeRemoteChanges(() => { store.put([recordToPut]) }) successCount++ console.log(`✅ Successfully loaded record ${record.id} (${record.typeName})`) } catch (individualError) { console.error(`❌ Failed to load record ${record.id} (${record.typeName}):`, individualError) console.log("Problematic record structure:", { id: record.id, typeName: record.typeName, type: record.type, hasW: 'w' in record, hasH: 'h' in record, w: record.w, h: record.h, propsW: record.props?.w, propsH: record.props?.h, allKeys: Object.keys(record) }) failedRecords.push(record) } } // Only log if there are failures or many records if (successCount < processedRecords.length || processedRecords.length > 50) { console.log(`Successfully loaded ${successCount} out of ${processedRecords.length} records`) } // Only log if debugging is needed // console.log(`Failed records: ${failedRecords.length}`, failedRecords.map(r => r.id)) // Try to fix and reload failed records if (failedRecords.length > 0) { // Only log if debugging is needed // console.log("Attempting to fix and reload failed records...") for (const record of failedRecords) { try { // Additional cleanup for failed records - create deep copy let fixedRecord = JSON.parse(JSON.stringify(record)) // Fix instance records specifically if (fixedRecord.typeName === 'instance') { if (!fixedRecord.meta) fixedRecord.meta = {} if (!Array.isArray(fixedRecord.insets)) { fixedRecord.insets = [false, false, false, false] } if (!Array.isArray(fixedRecord.scribbles)) { fixedRecord.scribbles = [] } if (typeof fixedRecord.duplicateProps !== 'object' || fixedRecord.duplicateProps === null) { fixedRecord.duplicateProps = {} } if (!Array.isArray(fixedRecord.duplicateProps.shapeIds)) { fixedRecord.duplicateProps.shapeIds = [] } if (typeof fixedRecord.duplicateProps.offset !== 'object' || fixedRecord.duplicateProps.offset === null) { fixedRecord.duplicateProps.offset = { x: 0, y: 0 } } } // Remove any remaining top-level w/h properties for shapes (except arrow, draw, and text) if (fixedRecord.typeName === 'shape') { if (fixedRecord.type !== 'arrow' && fixedRecord.type !== 'draw' && fixedRecord.type !== 'text') { if ('w' in fixedRecord) { if (!fixedRecord.props) fixedRecord.props = {} fixedRecord.props.w = fixedRecord.w delete (fixedRecord as any).w } if ('h' in fixedRecord) { if (!fixedRecord.props) fixedRecord.props = {} fixedRecord.props.h = fixedRecord.h delete (fixedRecord as any).h } } else if (fixedRecord.type === 'text') { // Text shapes only need w, not h if ('w' in fixedRecord) { if (!fixedRecord.props) fixedRecord.props = {} fixedRecord.props.w = fixedRecord.w delete (fixedRecord as any).w } if ('h' in fixedRecord) { delete (fixedRecord as any).h } } else { // For arrow and draw shapes, remove w/h entirely if ('w' in fixedRecord) { delete (fixedRecord as any).w } if ('h' in fixedRecord) { delete (fixedRecord as any).h } } } // Comprehensive richText validation - ensure it's always an object with content and type for text shapes if (fixedRecord.type === 'text' && fixedRecord.props) { if (fixedRecord.props.richText !== undefined) { if (!Array.isArray(fixedRecord.props.richText)) { console.log(`🔧 Fixing richText for text shape ${fixedRecord.id}: was ${typeof fixedRecord.props.richText}, setting to proper object`) fixedRecord.props.richText = { content: [], type: 'doc' } } else { // If it's an array, convert to proper richText object structure console.log(`🔧 Converting richText array to object for text shape ${fixedRecord.id}`) fixedRecord.props.richText = { content: fixedRecord.props.richText, type: 'doc' } } } else { // Text shapes must have richText as an object console.log(`🔧 Creating default richText object for text shape ${fixedRecord.id}`) fixedRecord.props.richText = { content: [], type: 'doc' } } } else if (fixedRecord.type === 'text' && !fixedRecord.props) { // Ensure props object exists for text shapes fixedRecord.props = { richText: { content: [], type: 'doc' } } } // Fix text shapes - ensure they have required properties including color if (fixedRecord.type === 'text') { if (!fixedRecord.props.color) { console.log(`🔧 Adding missing color property for text shape ${fixedRecord.id}`) fixedRecord.props.color = 'black' } if (!fixedRecord.props.size) { fixedRecord.props.size = 'm' } if (!fixedRecord.props.font) { fixedRecord.props.font = 'draw' } if (!fixedRecord.props.textAlign) { fixedRecord.props.textAlign = 'start' } if (!fixedRecord.props.w) { fixedRecord.props.w = 100 } if (fixedRecord.props.scale === undefined) { fixedRecord.props.scale = 1 } if (fixedRecord.props.autoSize === undefined) { fixedRecord.props.autoSize = false } if (!fixedRecord.props.richText) { console.log(`🔧 Creating default richText object for text shape ${fixedRecord.id}`) fixedRecord.props.richText = { content: [], type: 'doc' } } // Remove invalid properties for text shapes (matching default text shape schema) // Note: richText is actually required for text shapes, so don't remove it const invalidTextProps = ['h', 'geo', 'insets', 'scribbles', 'isMinimized', 'roomUrl', 'text', 'align', 'verticalAlign', 'growY', 'url'] invalidTextProps.forEach(prop => { if (prop in fixedRecord.props) { console.log(`🔧 Removing invalid prop '${prop}' from text shape ${fixedRecord.id}`) delete (fixedRecord.props as any)[prop] } }) } // Fix embed shapes - ensure they have required properties and remove invalid ones if (fixedRecord.type === 'Embed' || fixedRecord.type === 'embed') { if (!fixedRecord.props.url) { console.log(`🔧 Adding missing url property for embed shape ${fixedRecord.id}`) fixedRecord.props.url = '' } if (!fixedRecord.props.w) { fixedRecord.props.w = 400 } if (!fixedRecord.props.h) { fixedRecord.props.h = 300 } if (fixedRecord.props.isMinimized === undefined) { fixedRecord.props.isMinimized = false } // Remove invalid properties for embed shapes (matching custom EmbedShape schema) const invalidEmbedProps = ['doesResize', 'doesResizeHeight', 'roomUrl', 'roomId', 'color', 'fill', 'dash', 'size', 'text', 'font', 'align', 'verticalAlign', 'growY', 'richText'] invalidEmbedProps.forEach(prop => { if (prop in fixedRecord.props) { console.log(`🔧 Removing invalid prop '${prop}' from embed shape ${fixedRecord.id}`) delete (fixedRecord.props as any)[prop] } }) } // Remove any other problematic properties from shapes const invalidProps = ['insets', 'scribbles', 'geo'] invalidProps.forEach(prop => { if (prop in fixedRecord) { delete (fixedRecord as any)[prop] } }) // Final validation - ensure all required properties exist if (fixedRecord.typeName === 'shape') { // Ensure basic required properties if (fixedRecord.x === undefined) fixedRecord.x = 0 if (fixedRecord.y === undefined) fixedRecord.y = 0 if (fixedRecord.rotation === undefined) fixedRecord.rotation = 0 if (fixedRecord.isLocked === undefined) fixedRecord.isLocked = false if (fixedRecord.opacity === undefined) fixedRecord.opacity = 1 if (!fixedRecord.meta) fixedRecord.meta = {} // CRITICAL: Final geo shape validation - ALWAYS remove w/h/geo from top level if (fixedRecord.type === 'geo') { // Store values before removing from top level const wValue = 'w' in fixedRecord ? (fixedRecord as any).w : undefined const hValue = 'h' in fixedRecord ? (fixedRecord as any).h : undefined const geoValue = 'geo' in fixedRecord ? (fixedRecord as any).geo : undefined // Ensure props exists if (!fixedRecord.props) fixedRecord.props = {} // ALWAYS remove w from top level (even if value is 0 or undefined) if ('w' in fixedRecord) { if (!('w' in fixedRecord.props) || fixedRecord.props.w === undefined) { fixedRecord.props.w = wValue !== undefined ? wValue : 100 } delete (fixedRecord as any).w } // ALWAYS remove h from top level (even if value is 0 or undefined) if ('h' in fixedRecord) { if (!('h' in fixedRecord.props) || fixedRecord.props.h === undefined) { fixedRecord.props.h = hValue !== undefined ? hValue : 100 } delete (fixedRecord as any).h } // ALWAYS remove geo from top level (even if value is undefined) if ('geo' in fixedRecord) { if (!('geo' in fixedRecord.props) || fixedRecord.props.geo === undefined) { fixedRecord.props.geo = geoValue !== undefined ? geoValue : 'rectangle' } delete (fixedRecord as any).geo } // Ensure geo property exists in props if (!fixedRecord.props.geo) { fixedRecord.props.geo = 'rectangle' } // Ensure w and h are in props if (fixedRecord.props.w === undefined) fixedRecord.props.w = 100 if (fixedRecord.props.h === undefined) fixedRecord.props.h = 100 } // Ensure parentId exists if (!fixedRecord.parentId) { const pageRecord = records.find((r: any) => r.typeName === 'page') as any if (pageRecord && pageRecord.id) { fixedRecord.parentId = pageRecord.id } } // Ensure props object exists if (!fixedRecord.props) fixedRecord.props = {} // Ensure w and h exist in props (except for arrow, draw, line, text, note, and group shapes) if (fixedRecord.type !== 'arrow' && fixedRecord.type !== 'draw' && fixedRecord.type !== 'line' && fixedRecord.type !== 'text' && fixedRecord.type !== 'note' && fixedRecord.type !== 'group') { if (fixedRecord.props.w === undefined) fixedRecord.props.w = 100 if (fixedRecord.props.h === undefined) fixedRecord.props.h = 100 } else if (fixedRecord.type === 'text') { // Text shapes only need w, not h if (fixedRecord.props.w === undefined) fixedRecord.props.w = 100 } else if (fixedRecord.type === 'line') { // Line shapes should NOT have w or h properties if ('w' in fixedRecord.props) { console.log(`🔧 FINAL FIX: Removing invalid w property from line shape ${fixedRecord.id}`) delete fixedRecord.props.w } if ('h' in fixedRecord.props) { console.log(`🔧 FINAL FIX: Removing invalid h property from line shape ${fixedRecord.id}`) delete fixedRecord.props.h } // Ensure line shapes have valid points if (!fixedRecord.props.points || typeof fixedRecord.props.points !== 'object' || Array.isArray(fixedRecord.props.points)) { console.log(`🔧 FINAL FIX: Creating default points for line shape ${fixedRecord.id}`) fixedRecord.props.points = { 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } } } else { // Validate points const validPoints: Record = {} let pointIndex = 0 const indices = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10'] for (const [key, point] of Object.entries(fixedRecord.props.points)) { if (point && typeof point === 'object' && typeof (point as any).x === 'number' && typeof (point as any).y === 'number' && !isNaN((point as any).x) && !isNaN((point as any).y)) { const index = indices[pointIndex] || `a${pointIndex + 1}` validPoints[index] = { id: index, index: index as any, x: (point as any).x, y: (point as any).y } pointIndex++ } } if (Object.keys(validPoints).length === 0) { fixedRecord.props.points = { 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } } } else if (Object.keys(validPoints).length === 1) { const firstPoint = Object.values(validPoints)[0] const secondIndex = indices[1] || 'a2' validPoints[secondIndex] = { id: secondIndex, index: secondIndex as any, x: firstPoint.x + 100, y: firstPoint.y } fixedRecord.props.points = validPoints } else { fixedRecord.props.points = validPoints } } // Ensure other required line shape properties if (!fixedRecord.props.color) fixedRecord.props.color = 'black' if (!fixedRecord.props.dash) fixedRecord.props.dash = 'draw' if (!fixedRecord.props.size) fixedRecord.props.size = 'm' if (!fixedRecord.props.spline) fixedRecord.props.spline = 'line' if (fixedRecord.props.scale === undefined || fixedRecord.props.scale === null) { fixedRecord.props.scale = 1 } } else if (fixedRecord.type === 'note') { // Note shapes should NOT have w or h properties, but DO need richText if ('w' in fixedRecord.props) { console.log(`🔧 FINAL FIX: Removing invalid w property from note shape ${fixedRecord.id}`) delete fixedRecord.props.w } if ('h' in fixedRecord.props) { console.log(`🔧 FINAL FIX: Removing invalid h property from note shape ${fixedRecord.id}`) delete fixedRecord.props.h } // Note shapes REQUIRE richText property if (!fixedRecord.props.richText || typeof fixedRecord.props.richText !== 'object') { console.log(`🔧 FINAL FIX: Adding missing richText property for note shape ${fixedRecord.id}`) fixedRecord.props.richText = { content: [], type: 'doc' } } } else if (fixedRecord.type === 'group') { // Group shapes should NOT have w or h properties if ('w' in fixedRecord.props) { console.log(`🔧 FINAL FIX: Removing invalid w property from group shape ${fixedRecord.id}`) delete fixedRecord.props.w } if ('h' in fixedRecord.props) { console.log(`🔧 FINAL FIX: Removing invalid h property from group shape ${fixedRecord.id}`) delete fixedRecord.props.h } } } // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level if (fixedRecord.typeName === 'shape' && fixedRecord.type === 'geo') { // Store values before removing from top level const wValue = 'w' in fixedRecord ? (fixedRecord as any).w : undefined const hValue = 'h' in fixedRecord ? (fixedRecord as any).h : undefined const geoValue = 'geo' in fixedRecord ? (fixedRecord as any).geo : undefined // Create cleaned record without w/h/geo at top level const cleaned: any = {} for (const key in fixedRecord) { if (key !== 'w' && key !== 'h' && key !== 'geo') { cleaned[key] = (fixedRecord as any)[key] } } // Ensure props exists and move values there if needed if (!cleaned.props) cleaned.props = {} if (wValue !== undefined && (!('w' in cleaned.props) || cleaned.props.w === undefined)) { cleaned.props.w = wValue } if (hValue !== undefined && (!('h' in cleaned.props) || cleaned.props.h === undefined)) { cleaned.props.h = hValue } if (geoValue !== undefined && (!('geo' in cleaned.props) || cleaned.props.geo === undefined)) { cleaned.props.geo = geoValue } fixedRecord = cleaned as any } // CRITICAL: Final safety check - ensure text shapes don't have props.text (TLDraw schema doesn't allow it) // Text shapes should only use props.richText, not props.text if (fixedRecord.typeName === 'shape' && fixedRecord.type === 'text' && fixedRecord.props && 'text' in fixedRecord.props) { delete (fixedRecord.props as any).text } store.mergeRemoteChanges(() => { store.put([fixedRecord]) }) console.log(`✅ Successfully loaded fixed record ${fixedRecord.id}`) successCount++ } catch (retryError) { console.error(`❌ Still failed to load record ${record.id} after fix attempt:`, retryError) } } } } } // Verify loading const storeRecords = store.allRecords() const shapes = storeRecords.filter(r => r.typeName === 'shape') console.log(`📊 Store verification: ${processedRecords.length} processed records, ${storeRecords.length} total store records, ${shapes.length} shapes`) // Debug: Check if shapes have the right structure if (shapes.length > 0) { console.log("📊 Sample loaded shape:", { id: shapes[0].id, type: shapes[0].type, x: shapes[0].x, y: shapes[0].y, hasProps: !!shapes[0].props, propsKeys: shapes[0].props ? Object.keys(shapes[0].props) : [], allKeys: Object.keys(shapes[0]) }) // Validate all shapes have proper structure const invalidShapes = shapes.filter(shape => { const issues = [] if (!shape.props) issues.push('missing props') // Only check w/h for shapes that actually need them const shapesWithoutWH = ['arrow', 'draw', 'text', 'note', 'line'] if (!shapesWithoutWH.includes(shape.type) && (!(shape.props as any)?.w || !(shape.props as any)?.h)) { issues.push('missing w/h in props') } if ('w' in shape || 'h' in shape) { issues.push('w/h at top level instead of props') } return issues.length > 0 }) if (invalidShapes.length > 0) { console.warn(`⚠️ Found ${invalidShapes.length} shapes with structural issues:`, invalidShapes.map(s => ({ id: s.id, type: s.type, issues: { missingProps: !s.props, missingWH: s.type !== 'arrow' && s.type !== 'draw' && (!(s.props as any)?.w || !(s.props as any)?.h), topLevelWH: 'w' in s || 'h' in s } }))) } } // Debug: Check for any shapes that might have validation issues const shapesWithTopLevelW = shapes.filter(s => 'w' in s) const shapesWithTopLevelH = shapes.filter(s => 'h' in s) if (shapesWithTopLevelW.length > 0 || shapesWithTopLevelH.length > 0) { console.warn(`📊 Found ${shapesWithTopLevelW.length} shapes with top-level w, ${shapesWithTopLevelH.length} with top-level h`) // Fix shapes with top-level w/h properties shapesWithTopLevelW.forEach(shape => { console.log(`🔧 Fixing shape ${shape.id} with top-level w property`) if (!shape.props) shape.props = {} ;(shape.props as any).w = (shape as any).w delete (shape as any).w }) shapesWithTopLevelH.forEach(shape => { console.log(`🔧 Fixing shape ${shape.id} with top-level h property`) if (!shape.props) shape.props = {} ;(shape.props as any).h = (shape as any).h delete (shape as any).h }) } if (shapes.length === 0) { // Only log if debugging is needed // console.log("No store data found in Automerge document") } } // Only log if debugging is needed // console.log("Setting store status to synced-remote") setStoreWithStatus({ store, status: "synced-remote", connectionStatus: "online", }) } catch (error) { console.error("Error initializing store from Automerge:", error) // Try to recover by creating a minimal valid store try { console.log("Attempting to recover with minimal store...") const minimalStore = createTLStore({ schema: customSchema, }) // Add basic page and camera records minimalStore.mergeRemoteChanges(() => { minimalStore.put([ { id: 'page:page' as any, typeName: 'page', name: 'Page', index: 'a0' as any, meta: {} }, { id: 'camera:page:page' as any, typeName: 'camera', x: 0, y: 0, z: 1, meta: {} } ]) }) setStoreWithStatus({ store: minimalStore, status: "synced-remote", connectionStatus: "offline", error: error instanceof Error ? error : new Error("Store initialization failed, using minimal store") as any, }) } catch (recoveryError) { console.error("Failed to recover with minimal store:", recoveryError) setStoreWithStatus({ store, status: "not-synced", error: error instanceof Error ? error : new Error("Unknown error") as any, }) } } } initializeStore() return () => { unsubs.forEach((unsub) => unsub()) } }, [handle, store]) /* -------------------- Presence -------------------- */ // Create a safe handle that won't cause null errors const safeHandle = handle || { on: () => {}, off: () => {}, removeListener: () => {}, whenReady: () => Promise.resolve(), doc: () => null, change: () => {}, broadcast: () => {}, } as any const [, updateLocalState] = useLocalAwareness({ handle: safeHandle, userId: _userId, initialState: {}, }) const [peerStates] = useRemoteAwareness({ handle: safeHandle, localUserId: _userId, }) return { ...storeWithStatus, store, } as TLStoreWithStatus } // Presence hook (simplified version) export function useAutomergePresence(params: { handle: DocHandle | null store: any userMetadata: { userId: string name: string color: string } }) { const { handle, store, userMetadata } = params // Simple presence implementation useEffect(() => { if (!handle || !store) return const updatePresence = () => { // Basic presence update logic console.log("Updating presence for user:", userMetadata.userId) } updatePresence() }, [handle, store, userMetadata]) return { updatePresence: () => {}, presence: {}, } }