Merge pull request #13 from Jeff-Emmett/add-runpod-AI-API

prevent coordinate collapse on reload
This commit is contained in:
Jeff Emmett 2025-11-16 03:08:07 -07:00 committed by GitHub
commit cb5045984b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 234 additions and 12 deletions

View File

@ -3,7 +3,8 @@ import * as Automerge from "@automerge/automerge"
export function applyAutomergePatchesToTLStore(
patches: Automerge.Patch[],
store: TLStore
store: TLStore,
automergeDoc?: any // Optional Automerge document to read full records from
) {
const toRemove: TLRecord["id"][] = []
const updatedObjects: { [id: string]: TLRecord } = {}
@ -42,6 +43,29 @@ export function applyAutomergePatchesToTLStore(
}
}
// CRITICAL: If record doesn't exist in store yet, try to get it from Automerge document
// This prevents coordinates from defaulting to 0,0 when patches create new records
let automergeRecord: any = null
if (!existingRecord && automergeDoc && automergeDoc.store && automergeDoc.store[id]) {
try {
automergeRecord = automergeDoc.store[id]
// Extract coordinates from Automerge record if it's a shape
if (automergeRecord && automergeRecord.typeName === 'shape') {
const docX = automergeRecord.x
const docY = automergeRecord.y
if (typeof docX === 'number' && !isNaN(docX) && docX !== null && docX !== undefined) {
storeCoordinates.x = docX
}
if (typeof docY === 'number' && !isNaN(docY) && docY !== null && docY !== undefined) {
storeCoordinates.y = docY
}
}
} catch (e) {
// If we can't read from Automerge doc, continue without it
console.warn(`Could not read record ${id} from Automerge document:`, e)
}
}
// Infer typeName from ID pattern if record doesn't exist
let defaultTypeName = 'shape'
let defaultRecord: any = {
@ -112,7 +136,20 @@ export function applyAutomergePatchesToTLStore(
}
}
let record = updatedObjects[id] || (existingRecord ? JSON.parse(JSON.stringify(existingRecord)) : defaultRecord)
// CRITICAL: When creating a new record, prefer using the full record from Automerge document
// This ensures we get all properties including coordinates, not just defaults
let record: any
if (updatedObjects[id]) {
record = updatedObjects[id]
} else if (existingRecord) {
record = JSON.parse(JSON.stringify(existingRecord))
} else if (automergeRecord) {
// Use the full record from Automerge document - this has all properties including coordinates
record = JSON.parse(JSON.stringify(automergeRecord))
} else {
// Fallback to default record only if we can't get it from anywhere else
record = defaultRecord
}
// CRITICAL: For shapes, ensure x and y are always present (even if record came from updatedObjects)
// This prevents coordinates from being lost when records are created from patches
@ -157,6 +194,28 @@ export function applyAutomergePatchesToTLStore(
const originalX = storeCoordinates.x !== undefined ? storeCoordinates.x : recordX
const originalY = storeCoordinates.y !== undefined ? storeCoordinates.y : recordY
const hadOriginalCoordinates = originalX !== undefined && originalY !== undefined
// CRITICAL: Store original richText and arrow text before patch application to preserve them
// This ensures richText and arrow text aren't lost when patches only update other properties
let originalRichText: any = undefined
let originalArrowText: any = undefined
if (record.typeName === 'shape') {
// Get richText from store's current state (most reliable)
if (existingRecord && (existingRecord as any).props && (existingRecord as any).props.richText) {
originalRichText = (existingRecord as any).props.richText
} else if ((record as any).props && (record as any).props.richText) {
originalRichText = (record as any).props.richText
}
// Get arrow text from store's current state (most reliable)
if ((record as any).type === 'arrow') {
if (existingRecord && (existingRecord as any).props && (existingRecord as any).props.text !== undefined) {
originalArrowText = (existingRecord as any).props.text
} else if ((record as any).props && (record as any).props.text !== undefined) {
originalArrowText = (record as any).props.text
}
}
}
switch (patch.action) {
case "insert": {
@ -229,6 +288,42 @@ export function applyAutomergePatchesToTLStore(
updatedObjects[id] = { ...updatedObjects[id], y: defaultRecord.y || 0 } as TLRecord
}
}
// CRITICAL: Preserve richText and arrow text after patch application
// This prevents richText and arrow text from being lost when patches only update other properties
const currentRecord = updatedObjects[id]
// Preserve richText for geo/note/text shapes
if (originalRichText !== undefined && (currentRecord as any).type !== 'arrow') {
const patchedProps = (currentRecord as any).props || {}
const patchedRichText = patchedProps.richText
// If patch didn't include richText, preserve the original
if (patchedRichText === undefined || patchedRichText === null) {
updatedObjects[id] = {
...currentRecord,
props: {
...patchedProps,
richText: originalRichText
}
} as TLRecord
}
}
// Preserve arrow text for arrow shapes
if (originalArrowText !== undefined && (currentRecord as any).type === 'arrow') {
const patchedProps = (currentRecord as any).props || {}
const patchedText = patchedProps.text
// If patch didn't include text, preserve the original
if (patchedText === undefined || patchedText === null) {
updatedObjects[id] = {
...currentRecord,
props: {
...patchedProps,
text: originalArrowText
}
} as TLRecord
}
}
}
// CRITICAL: Re-check typeName after patch application to ensure it's still correct
@ -660,6 +755,46 @@ export function sanitizeRecord(record: any): TLRecord {
sanitized.props.richText = cleanRichTextNaN(sanitized.props.richText)
}
// CRITICAL: Preserve arrow text property (ensure it's a string)
if (sanitized.type === 'arrow') {
// Ensure text property exists and is a string
if (sanitized.props.text === undefined || sanitized.props.text === null) {
sanitized.props.text = ''
} else if (typeof sanitized.props.text !== 'string') {
// If text is not a string (e.g., RichText object), convert it to string
try {
if (typeof sanitized.props.text === 'object' && sanitized.props.text !== null) {
// Try to extract text from RichText object
const textObj = sanitized.props.text as any
if (Array.isArray(textObj.content)) {
// Extract text from RichText content
const extractText = (content: any[]): string => {
return content.map((item: any) => {
if (item.type === 'text' && item.text) {
return item.text
} else if (item.content && Array.isArray(item.content)) {
return extractText(item.content)
}
return ''
}).join('')
}
sanitized.props.text = extractText(textObj.content)
} else if (textObj.text && typeof textObj.text === 'string') {
sanitized.props.text = textObj.text
} else {
sanitized.props.text = String(sanitized.props.text)
}
} else {
sanitized.props.text = String(sanitized.props.text)
}
} catch (e) {
console.warn(`⚠️ AutomergeToTLStore: Error converting arrow text to string for ${sanitized.id}:`, e)
sanitized.props.text = String(sanitized.props.text)
}
}
// Note: We preserve text even if it's an empty string - that's a valid value
}
// CRITICAL: Fix richText structure for text shapes - REQUIRED field
if (sanitized.type === 'text') {
// Text shapes MUST have props.richText as an object - initialize if missing

View File

@ -144,19 +144,97 @@ function sanitizeRecord(record: TLRecord): TLRecord {
console.warn(`🔧 TLStoreToAutomerge: Error checking richText for shape ${sanitized.id}:`, e)
}
// CRITICAL: Extract arrow text BEFORE deep copy to handle RichText instances properly
// Arrow text should be a string, but might be a RichText object in edge cases
let arrowTextValue: any = undefined
if (sanitized.type === 'arrow') {
try {
const props = sanitized.props || {}
if ('text' in props) {
try {
// Use Object.getOwnPropertyDescriptor to safely check if it's a getter
const descriptor = Object.getOwnPropertyDescriptor(props, 'text')
let textValue: any = undefined
if (descriptor && descriptor.get) {
// It's a getter - try to call it safely
try {
textValue = descriptor.get.call(props)
} catch (getterError) {
console.warn(`🔧 TLStoreToAutomerge: Error calling text getter for arrow ${sanitized.id}:`, getterError)
textValue = undefined
}
} else {
// It's a regular property - access it directly
textValue = (props as any).text
}
// Now process the value
if (textValue !== undefined && textValue !== null) {
// If it's a string, use it directly
if (typeof textValue === 'string') {
arrowTextValue = textValue
}
// If it's a RichText object, extract the text content
else if (typeof textValue === 'object' && textValue !== null) {
// Try to extract text from RichText object
try {
const serialized = JSON.parse(JSON.stringify(textValue))
// If it has content array, extract text from it
if (Array.isArray(serialized.content)) {
// Extract text from RichText content
const extractText = (content: any[]): string => {
return content.map((item: any) => {
if (item.type === 'text' && item.text) {
return item.text
} else if (item.content && Array.isArray(item.content)) {
return extractText(item.content)
}
return ''
}).join('')
}
arrowTextValue = extractText(serialized.content)
} else {
// Fallback: try to get text property
arrowTextValue = serialized.text || ''
}
} catch (serializeError) {
// If serialization fails, try to extract manually
if ((textValue as any).text && typeof (textValue as any).text === 'string') {
arrowTextValue = (textValue as any).text
} else {
arrowTextValue = String(textValue)
}
}
}
// For other types, convert to string
else {
arrowTextValue = String(textValue)
}
}
} catch (e) {
console.warn(`🔧 TLStoreToAutomerge: Error extracting text for arrow ${sanitized.id}:`, e)
arrowTextValue = undefined
}
}
} catch (e) {
console.warn(`🔧 TLStoreToAutomerge: Error checking text for arrow ${sanitized.id}:`, e)
}
}
// CRITICAL: For all shapes, ensure props is a deep mutable copy to preserve all properties
// This is essential for custom shapes like ObsNote and for preserving richText in geo shapes
// Use JSON parse/stringify to create a deep copy of nested objects (like richText.content)
// Remove richText temporarily to avoid serialization issues
// Remove richText and arrow text temporarily to avoid serialization issues
try {
const propsWithoutRichText: any = {}
// Copy all props except richText
const propsWithoutSpecial: any = {}
// Copy all props except richText and arrow text (if extracted)
for (const key in sanitized.props) {
if (key !== 'richText') {
propsWithoutRichText[key] = (sanitized.props as any)[key]
if (key !== 'richText' && !(sanitized.type === 'arrow' && key === 'text' && arrowTextValue !== undefined)) {
propsWithoutSpecial[key] = (sanitized.props as any)[key]
}
}
sanitized.props = JSON.parse(JSON.stringify(propsWithoutRichText))
sanitized.props = JSON.parse(JSON.stringify(propsWithoutSpecial))
} catch (e) {
console.warn(`🔧 TLStoreToAutomerge: Error deep copying props for shape ${sanitized.id}:`, e)
// Fallback: just copy props without deep copy
@ -164,6 +242,9 @@ function sanitizeRecord(record: TLRecord): TLRecord {
if (richTextValue !== undefined) {
delete (sanitized.props as any).richText
}
if (arrowTextValue !== undefined) {
delete (sanitized.props as any).text
}
}
// CRITICAL: For geo shapes, move w/h/geo from top-level to props (required by TLDraw schema)
@ -210,11 +291,17 @@ function sanitizeRecord(record: TLRecord): TLRecord {
// CRITICAL: For arrow shapes, preserve text property
if (sanitized.type === 'arrow') {
// CRITICAL: Preserve text property - only set default if truly missing (preserve empty strings and all other values)
if ((sanitized.props as any).text === undefined || (sanitized.props as any).text === null) {
(sanitized.props as any).text = ''
// CRITICAL: Restore extracted text value if available, otherwise preserve existing text
if (arrowTextValue !== undefined) {
// Use the extracted text value (handles RichText objects by extracting text content)
(sanitized.props as any).text = arrowTextValue
} else {
// CRITICAL: Preserve text property - only set default if truly missing (preserve empty strings and all other values)
if ((sanitized.props as any).text === undefined || (sanitized.props as any).text === null) {
(sanitized.props as any).text = ''
}
// Note: We preserve text even if it's an empty string - that's a valid value
}
// Note: We preserve text even if it's an empty string - that's a valid value
}
// CRITICAL: For note shapes, preserve richText property (required for note shapes)