From 92036d3c1e233708e6243afd92ad13231530dd6b Mon Sep 17 00:00:00 2001 From: qxip Date: Tue, 10 Mar 2026 01:10:23 +0100 Subject: [PATCH 1/2] fix: Styles/References API fields + file:// URLs; add ErrorBoundary for on-screen errors - Styles: use reference_image_path, pathToFileUrl for images, send reference_image_path in body - References: use image_path, pathToFileUrl for images, send image_path in body - Characters: already fixed (reference_image_path, pathToFileUrl) - Add ErrorBoundary to catch render errors and show message/stack on screen; log via sendRendererLog Made-with: Cursor --- frontend/App.tsx | 19 +++--- frontend/components/ErrorBoundary.tsx | 86 +++++++++++++++++++++++++++ frontend/views/Characters.tsx | 42 +++++++++---- frontend/views/References.tsx | 39 ++++++++---- frontend/views/Styles.tsx | 31 +++++++--- 5 files changed, 180 insertions(+), 37 deletions(-) create mode 100644 frontend/components/ErrorBoundary.tsx diff --git a/frontend/App.tsx b/frontend/App.tsx index 2afe4960..74911d8b 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -21,6 +21,7 @@ import { PythonSetup } from './components/PythonSetup' import { SettingsModal, type SettingsTabId } from './components/SettingsModal' import { LogViewer } from './components/LogViewer' import { ApiGatewayModal, type ApiGatewaySection } from './components/ApiGatewayModal' +import { ErrorBoundary } from './components/ErrorBoundary' import { Button } from './components/ui/button' type SetupState = 'loading' | { needsSetup: boolean; needsLicense: boolean } @@ -545,13 +546,15 @@ function AppContent() { export default function App() { return ( - - - - - - - - + + + + + + + + + + ) } diff --git a/frontend/components/ErrorBoundary.tsx b/frontend/components/ErrorBoundary.tsx new file mode 100644 index 00000000..5810db59 --- /dev/null +++ b/frontend/components/ErrorBoundary.tsx @@ -0,0 +1,86 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react' +import { AlertCircle } from 'lucide-react' +import { Button } from './ui/button' + +interface Props { + children: ReactNode + /** Optional label for the fallback (e.g. "Something went wrong") */ + fallbackTitle?: string +} + +interface State { + error: Error | null + errorInfo: ErrorInfo | null +} + +/** + * Catches React render errors and shows the message (and stack) on screen + * so we're not blind when the app crashes (e.g. without DevTools). + */ +export class ErrorBoundary extends Component { + state: State = { error: null, errorInfo: null } + + static getDerivedStateFromError(error: Error): Partial { + return { error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + this.setState({ errorInfo }) + if (typeof window !== 'undefined' && window.electronAPI?.sendRendererLog) { + window.electronAPI.sendRendererLog('error', '[ErrorBoundary]', error?.message, error?.stack, errorInfo.componentStack) + } + } + + render(): ReactNode { + const { error, errorInfo } = this.state + if (!error) return this.props.children + + const title = this.props.fallbackTitle ?? 'Something went wrong' + const message = error.message ?? String(error) + const stack = error.stack ?? '' + const componentStack = errorInfo?.componentStack ?? '' + + return ( +
+
+
+ +
+

{title}

+

{message}

+ {stack && ( +
+                  {stack}
+                
+ )} + {componentStack && ( +
+ Component stack +
+                    {componentStack}
+                  
+
+ )} +
+ + +
+
+
+
+
+ ) + } +} diff --git a/frontend/views/Characters.tsx b/frontend/views/Characters.tsx index 60036b59..873764ff 100644 --- a/frontend/views/Characters.tsx +++ b/frontend/views/Characters.tsx @@ -5,15 +5,26 @@ import { LtxLogo } from '../components/LtxLogo' import { Button } from '../components/ui/button' import { logger } from '../lib/logger' +/** Matches API: reference_image_paths are filesystem paths; we convert to file:// for */ interface Character { id: string name: string role: string description: string - reference_images: string[] + reference_image_paths: string[] created_at: string } +function pathToFileUrl(filePath: string): string { + const normalized = filePath.replace(/\\/g, '/') + return normalized.startsWith('/') ? `file://${normalized}` : `file:///${normalized}` +} + +function safeImagePaths(raw: unknown): string[] { + if (!Array.isArray(raw)) return [] + return raw.filter((x): x is string => typeof x === 'string' && x.length > 0) +} + export function Characters() { const { goHome } = useProjects() const [characters, setCharacters] = useState([]) @@ -34,8 +45,17 @@ export function Characters() { const backendUrl = await window.electronAPI.getBackendUrl() const res = await fetch(`${backendUrl}/api/library/characters`) if (!res.ok) throw new Error(`Failed to fetch characters: ${res.status}`) - const data = (await res.json()) as { characters: Character[] } - setCharacters(data.characters ?? []) + const data = (await res.json()) as { characters: unknown[] } + setCharacters( + (data.characters ?? []).map((c: Record) => ({ + id: String(c.id ?? ''), + name: String(c.name ?? ''), + role: String(c.role ?? ''), + description: String(c.description ?? ''), + reference_image_paths: safeImagePaths(c.reference_image_paths ?? c.reference_images ?? []), + created_at: String(c.created_at ?? ''), + })) + ) } catch (e) { const msg = e instanceof Error ? e.message : 'Failed to load characters' logger.error(msg) @@ -63,7 +83,7 @@ export function Characters() { setFormName(char.name) setFormRole(char.role) setFormDescription(char.description) - setFormImages([...char.reference_images]) + setFormImages([...char.reference_image_paths]) setIsModalOpen(true) } @@ -76,7 +96,7 @@ export function Characters() { name: formName.trim(), role: formRole.trim(), description: formDescription.trim(), - reference_images: formImages, + reference_image_paths: formImages, } if (editingCharacter) { const res = await fetch(`${backendUrl}/api/library/characters/${editingCharacter.id}`, { @@ -182,14 +202,14 @@ export function Characters() { key={char.id} className="group bg-zinc-900 rounded-lg border border-zinc-800 hover:border-zinc-600 transition-all overflow-hidden" > - {/* Reference images */} + {/* Reference images (paths → file:// for renderer) */}
- {char.reference_images.length > 0 ? ( + {char.reference_image_paths.length > 0 ? (
- {char.reference_images.slice(0, 4).map((img, i) => ( + {char.reference_image_paths.slice(0, 4).map((path, i) => ( {`${char.name} @@ -283,9 +303,9 @@ export function Characters() {
- {formImages.map((img, i) => ( + {formImages.map((path, i) => (
- +