mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 10:17:43 +02:00
[eric] chat: assistant answers drop the bubble (user-only bubbles); tool-ui cards get skeleton + blur-in entrance
This commit is contained in:
@@ -26,7 +26,22 @@ function ToolUiBubble({ pair, sessionId, isPending, suppressReveal, sessionRunni
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box sx={{ my: 1, contain: 'layout style' }} data-select-type="tool-ui" data-select-id={pair.id} data-select-meta={JSON.stringify({ component: payload.component })}>
|
||||
<Box
|
||||
sx={{
|
||||
my: 1,
|
||||
contain: 'layout style',
|
||||
// One-shot entrance (assistant-ui's fade + rise + blur-in): the card arrives, it doesn't pop.
|
||||
animation: 'toolUiEnter 240ms cubic-bezier(0.32, 0.72, 0, 1)',
|
||||
'@keyframes toolUiEnter': {
|
||||
from: { opacity: 0, transform: 'translateY(6px)', filter: 'blur(2px)' },
|
||||
to: { opacity: 1, transform: 'translateY(0)', filter: 'blur(0)' },
|
||||
},
|
||||
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
||||
}}
|
||||
data-select-type="tool-ui"
|
||||
data-select-id={pair.id}
|
||||
data-select-meta={JSON.stringify({ component: payload.component })}
|
||||
>
|
||||
<ShowUiWidgetView payload={payload} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -60,6 +60,29 @@ function parseLeniently(schema: { safeParse: (v: unknown) => any }, props: Recor
|
||||
return { state: 'bad', problem: issues };
|
||||
}
|
||||
|
||||
// Rough resting height per component family so the loading skeleton reserves believable space
|
||||
// (Lobe/Open WebUI pattern: a breathing block where the card will land, not a tiny sliver).
|
||||
function skeletonHeightFor(name: string): number {
|
||||
if (/table|chart|gallery|map|carousel|post|terminal/.test(name)) return 180;
|
||||
if (/stats|weather|plan|order|preferences|question/.test(name)) return 110;
|
||||
return 56;
|
||||
}
|
||||
|
||||
const SkeletonBlock: React.FC<{ name: string }> = ({ name }) => (
|
||||
<div
|
||||
style={{
|
||||
height: skeletonHeightFor(name),
|
||||
width: '100%',
|
||||
maxWidth: 520,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(127,127,127,0.12)',
|
||||
animation: 'toolui-skeleton-pulse 1.4s ease-in-out infinite',
|
||||
}}
|
||||
>
|
||||
<style>{'@keyframes toolui-skeleton-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }'}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** Validates against the upstream zod contract, then renders the vendored component inside the scoped theme. */
|
||||
function VendoredToolUi({ name, props, extraProps }: VendoredToolUiProps): React.ReactElement | null {
|
||||
const { mode } = useThemeMode();
|
||||
@@ -87,13 +110,13 @@ function VendoredToolUi({ name, props, extraProps }: VendoredToolUiProps): React
|
||||
);
|
||||
}
|
||||
if (gate.state === 'pending') {
|
||||
return <div style={{ height: 48, width: 280, borderRadius: 12, background: 'rgba(127,127,127,0.12)' }} />;
|
||||
return <SkeletonBlock name={name} />;
|
||||
}
|
||||
const Component = entry.Component;
|
||||
return (
|
||||
<div className={`tool-ui-scope${mode === 'dark' ? ' dark' : ''}`}>
|
||||
<ComponentGuard name={name}>
|
||||
<Suspense fallback={<div style={{ height: 48, width: 280, borderRadius: 12, background: 'rgba(127,127,127,0.12)' }} />}>
|
||||
<Suspense fallback={<SkeletonBlock name={name} />}>
|
||||
<Component {...gate.parsed} {...(extraProps || {})} />
|
||||
</Suspense>
|
||||
</ComponentGuard>
|
||||
|
||||
Reference in New Issue
Block a user