mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-29 13:04:51 +02:00
[eric] ui-tools: live in-place card updates (same component+id); social posts wrapped as {post} (flat props crashed the shell); per-component error wall; item-carousel validates the carousel shape
This commit is contained in:
@@ -2,6 +2,33 @@ import React, { Suspense, useEffect, useState } from 'react';
|
||||
import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
import { TOOL_UI_REGISTRY } from './registry';
|
||||
|
||||
interface GuardProps { name: string; children: React.ReactNode }
|
||||
|
||||
// A component render throwing must cost exactly one quiet line, never the app: the top-level
|
||||
// ErrorBoundary unmounts the whole shell for any uncaught child throw (the linkedin-post {post}
|
||||
// mismatch took down the dashboard until this wall existed).
|
||||
class ComponentGuard extends React.Component<GuardProps, { failed: boolean }> {
|
||||
constructor(props: GuardProps) {
|
||||
super(props);
|
||||
this.state = { failed: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(): { failed: boolean } {
|
||||
return { failed: true };
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.failed) {
|
||||
return (
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.55, padding: '4px 0' }}>
|
||||
{this.props.name} failed to render
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
interface VendoredToolUiProps {
|
||||
name: string;
|
||||
props: Record<string, unknown>;
|
||||
@@ -65,9 +92,11 @@ function VendoredToolUi({ name, props, extraProps }: VendoredToolUiProps): React
|
||||
const Component = entry.Component;
|
||||
return (
|
||||
<div className={`tool-ui-scope${mode === 'dark' ? ' dark' : ''}`}>
|
||||
<Suspense fallback={<div style={{ height: 48, width: 280, borderRadius: 12, background: 'rgba(127,127,127,0.12)' }} />}>
|
||||
<Component {...gate.parsed} {...(extraProps || {})} />
|
||||
</Suspense>
|
||||
<ComponentGuard name={name}>
|
||||
<Suspense fallback={<div style={{ height: 48, width: 280, borderRadius: 12, background: 'rgba(127,127,127,0.12)' }} />}>
|
||||
<Component {...gate.parsed} {...(extraProps || {})} />
|
||||
</Suspense>
|
||||
</ComponentGuard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ export interface ToolUiEntry {
|
||||
|
||||
/* Every entry lazy-loads both the component and its zod contract so the chat bundle only pays
|
||||
for components a transcript actually uses. Names mirror upstream tool-ui component slugs. */
|
||||
// The social-post components take their data nested as {post}; the wire props ARE the post.
|
||||
function wrapAsPost<P extends { id?: unknown }>(Inner: React.ComponentType<{ post: P }>): React.ComponentType<P> {
|
||||
return function PostAdapter(props: P) {
|
||||
return <Inner post={props} />;
|
||||
};
|
||||
}
|
||||
|
||||
export const TOOL_UI_REGISTRY: Record<string, ToolUiEntry> = {
|
||||
'audio': {
|
||||
Component: lazy(() => import('./components/audio').then((m) => ({ default: m.Audio }))),
|
||||
@@ -52,19 +59,19 @@ export const TOOL_UI_REGISTRY: Record<string, ToolUiEntry> = {
|
||||
loadSchema: () => import('./components/image-gallery/schema').then((m) => m.SerializableImageGallerySchema),
|
||||
},
|
||||
'instagram-post': {
|
||||
Component: lazy(() => import('./components/instagram-post').then((m) => ({ default: m.InstagramPost }))),
|
||||
Component: lazy(() => import('./components/instagram-post').then((m) => ({ default: wrapAsPost(m.InstagramPost) }))),
|
||||
loadSchema: () => import('./components/instagram-post/schema').then((m) => m.SerializableInstagramPostSchema),
|
||||
},
|
||||
'item-carousel': {
|
||||
Component: lazy(() => import('./components/item-carousel').then((m) => ({ default: m.ItemCarousel }))),
|
||||
loadSchema: () => import('./components/item-carousel/schema').then((m) => m.SerializableItemSchema),
|
||||
loadSchema: () => import('./components/item-carousel/schema').then((m) => m.SerializableItemCarouselSchema),
|
||||
},
|
||||
'link-preview': {
|
||||
Component: lazy(() => import('./components/link-preview').then((m) => ({ default: m.LinkPreview }))),
|
||||
loadSchema: () => import('./components/link-preview/schema').then((m) => m.SerializableLinkPreviewSchema),
|
||||
},
|
||||
'linkedin-post': {
|
||||
Component: lazy(() => import('./components/linkedin-post').then((m) => ({ default: m.LinkedInPost }))),
|
||||
Component: lazy(() => import('./components/linkedin-post').then((m) => ({ default: wrapAsPost(m.LinkedInPost) }))),
|
||||
loadSchema: () => import('./components/linkedin-post/schema').then((m) => m.SerializableLinkedInPostSchema),
|
||||
},
|
||||
'message-draft': {
|
||||
@@ -112,7 +119,7 @@ export const TOOL_UI_REGISTRY: Record<string, ToolUiEntry> = {
|
||||
loadSchema: () => import('./components/video/schema').then((m) => m.SerializableVideoSchema),
|
||||
},
|
||||
'x-post': {
|
||||
Component: lazy(() => import('./components/x-post').then((m) => ({ default: m.XPost }))),
|
||||
Component: lazy(() => import('./components/x-post').then((m) => ({ default: wrapAsPost(m.XPost) }))),
|
||||
loadSchema: () => import('./components/x-post/schema').then((m) => m.SerializableXPostSchema),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user