From 090646f893d22c434cf5687a3dd818015b9b0a52 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 30 Nov 2025 20:26:51 -0800 Subject: [PATCH] fix: sanitize shape indices and improve RunPod error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/routes/Board.tsx | 38 +++++++++++++++++++++++++------- src/shapes/ImageGenShapeUtil.tsx | 16 +++++++++++++- wrangler.toml | 12 +++++++++- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index be7f9ab..f667ee4 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -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 diff --git a/src/shapes/ImageGenShapeUtil.tsx b/src/shapes/ImageGenShapeUtil.tsx index 2397397..e9fdfd7 100644 --- a/src/shapes/ImageGenShapeUtil.tsx +++ b/src/shapes/ImageGenShapeUtil.tsx @@ -433,8 +433,22 @@ export class ImageGenShape extends BaseBoxShapeUtil { } } 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) diff --git a/wrangler.toml b/wrangler.toml index 1655a35..9532a26 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -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"