fix: sanitize shape indices and improve RunPod error handling

- Add index sanitization in Board.tsx to fix "Expected an index key"
  validation errors when selecting shapes with old format indices
- Improve RunPod error handling to properly display status messages
  (IN_PROGRESS, IN_QUEUE, FAILED) instead of generic errors
- Update wrangler.toml with current compatibility date and document
  RunPod endpoint configuration for reference
- Add sanitizeIndex helper function to convert invalid indices like
  "b1" to valid tldraw fractional indices like "a1"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-11-30 20:26:51 -08:00
parent 829dd4f642
commit 090646f893
3 changed files with 56 additions and 10 deletions

View File

@ -1,7 +1,7 @@
import { useAutomergeSync } from "@/automerge/useAutomergeSync"
import { AutomergeHandleProvider } from "@/context/AutomergeHandleContext"
import { useMemo, useEffect, useState, useRef } from "react"
import { Tldraw, Editor, TLShapeId, TLRecord, useTldrawUser, TLUserPreferences } from "tldraw"
import { Tldraw, Editor, TLShapeId, TLRecord, useTldrawUser, TLUserPreferences, IndexKey } from "tldraw"
import { useParams } from "react-router-dom"
import { ChatBoxTool } from "@/tools/ChatBoxTool"
import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil"
@ -66,6 +66,24 @@ import "react-cmdk/dist/cmdk.css"
import "@/css/style.css"
import "@/css/obsidian-browser.css"
// Helper to validate and fix tldraw IndexKey format
// Valid: "a0", "a1", "a24sT", "a1V4rr" - Invalid: "b1", "c1" (old format)
function sanitizeIndex(index: any): IndexKey {
if (!index || typeof index !== 'string' || index.length === 0) {
return 'a1' as IndexKey
}
// Old format "b1", "c1" etc are invalid (single letter + single digit)
if (/^[b-z]\d$/i.test(index)) {
return 'a1' as IndexKey
}
// Valid: starts with 'a' followed by at least one digit
if (/^a\d/.test(index)) {
return index as IndexKey
}
// Fallback
return 'a1' as IndexKey
}
const collections: Collection[] = [GraphLayoutCollection]
import { useAuth } from "../context/AuthContext"
import { updateLastVisited } from "../lib/starredBoards"
@ -594,33 +612,37 @@ export function Board() {
// Fallback if store not available
const fallbackX = (s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x)) ? s.x : 0
const fallbackY = (s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y)) ? s.y : 0
return { ...s, parentId: currentPageId, x: fallbackX, y: fallbackY } as TLRecord
// CRITICAL: Sanitize index to prevent validation errors
return { ...s, parentId: currentPageId, x: fallbackX, y: fallbackY, index: sanitizeIndex(s.index) } as TLRecord
}
const shapeFromStore = store.store.get(s.id)
if (shapeFromStore && shapeFromStore.typeName === 'shape') {
// CRITICAL: Get coordinates from store's current state (most reliable)
// This ensures we preserve coordinates even if the shape object has been modified
const storeX = (shapeFromStore as any).x
const storeY = (shapeFromStore as any).y
const originalX = (typeof storeX === 'number' && !isNaN(storeX) && storeX !== null && storeX !== undefined)
? storeX
const originalX = (typeof storeX === 'number' && !isNaN(storeX) && storeX !== null && storeX !== undefined)
? storeX
: (s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x) ? s.x : 0)
const originalY = (typeof storeY === 'number' && !isNaN(storeY) && storeY !== null && storeY !== undefined)
? storeY
: (s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y) ? s.y : 0)
// Create fixed shape with preserved coordinates
// Create fixed shape with preserved coordinates and sanitized index
const fixed: any = { ...shapeFromStore, parentId: currentPageId }
// CRITICAL: Always preserve coordinates - never reset to 0,0 unless truly missing
fixed.x = originalX
fixed.y = originalY
// CRITICAL: Sanitize index to prevent "Expected an index key" validation errors
fixed.index = sanitizeIndex(fixed.index)
return fixed as TLRecord
}
// Fallback if shape not in store - preserve coordinates from s
const fallbackX = (s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x)) ? s.x : 0
const fallbackY = (s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y)) ? s.y : 0
return { ...s, parentId: currentPageId, x: fallbackX, y: fallbackY } as TLRecord
// CRITICAL: Sanitize index to prevent validation errors
return { ...s, parentId: currentPageId, x: fallbackX, y: fallbackY, index: sanitizeIndex(s.index) } as TLRecord
})
try {
// CRITICAL: Use mergeRemoteChanges to prevent feedback loop

View File

@ -433,8 +433,22 @@ export class ImageGenShape extends BaseBoxShapeUtil<IImageGen> {
}
} else if (data.error) {
throw new Error(`RunPod API error: ${data.error}`)
} else if (data.status) {
// Handle RunPod status responses (no output yet)
const status = data.status.toUpperCase()
if (status === 'IN_PROGRESS' || status === 'IN_QUEUE') {
throw new Error(`Image generation timed out (status: ${data.status}). The GPU may be experiencing a cold start. Please try again in a moment.`)
} else if (status === 'FAILED') {
throw new Error(`RunPod job failed: ${data.error || 'Unknown error'}`)
} else if (status === 'CANCELLED') {
throw new Error('Image generation was cancelled')
} else {
throw new Error(`Unexpected RunPod status: ${data.status}`)
}
} else {
throw new Error("No valid response from RunPod API - missing output field")
// Log full response for debugging
console.error("❌ ImageGen: Unexpected response structure:", JSON.stringify(data, null, 2))
throw new Error("No valid response from RunPod API - missing output field. Check console for details.")
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)

View File

@ -1,8 +1,9 @@
# Worker configuration
# Note: This wrangler.toml is for the Worker backend only.
# Pages deployment is configured separately in the Cloudflare dashboard.
# Frontend (Vite) environment variables should be set in Cloudflare Pages dashboard.
main = "worker/worker.ts"
compatibility_date = "2024-07-01"
compatibility_date = "2024-11-01"
name = "jeffemmett-canvas"
account_id = "0e7b3338d5278ed1b148e6456b940913"
@ -11,6 +12,15 @@ account_id = "0e7b3338d5278ed1b148e6456b940913"
# Workers & Pages → jeffemmett-canvas → Settings → Variables
DAILY_DOMAIN = "mycopunks.daily.co"
# RunPod AI Service Configuration (defaults hardcoded in src/lib/clientConfig.ts)
# These are documented here for reference - actual values are in the client code
# to allow all users to access AI features without configuration
# RUNPOD_API_KEY = "set via wrangler secret put"
# RUNPOD_IMAGE_ENDPOINT_ID = "tzf1j3sc3zufsy" # Automatic1111
# RUNPOD_VIDEO_ENDPOINT_ID = "4jql4l7l0yw0f3" # Wan2.2
# RUNPOD_TEXT_ENDPOINT_ID = "03g5hz3hlo8gr2" # vLLM
# RUNPOD_WHISPER_ENDPOINT_ID = "lrtisuv8ixbtub" # Whisper
[dev]
port = 5172
ip = "0.0.0.0"