Skip to content

Commit 28b3d4a

Browse files
committed
fix: loading for components
1 parent a46e696 commit 28b3d4a

File tree

5 files changed

+65
-33
lines changed

5 files changed

+65
-33
lines changed

.agents/skills/databuddy/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Read [codebase-map.md](./references/codebase-map.md) when you need deeper routin
128128
- **AI SDK UI (`useChat`)** does not document automatic HTTP retries on `DefaultChatTransport`—retry UX is **`regenerate()`** + `error` ([chatbot error state](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#error-state), [error handling](https://ai-sdk.dev/docs/ai-sdk-ui/error-handling)). `maxRetries` on **`streamText`/`generateText`** is server-side model calls, not the browser chat `fetch`. Mid-stream disconnect: **`resumeStream()`** ([useChat](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)).
129129
- **`@elysiajs/cors` with `origin: true`** sets `Vary: *`, killing CDN caching. Override with `set.headers.vary = "Origin"` on cacheable public endpoints.
130130
- **`applyAuthWideEvent`** in `apps/api/src/index.ts` runs a session DB lookup on every request including anonymous `/public/` routes. Skip it for public endpoints via URL check in `onBeforeHandle`.
131-
- **Agent SQL security**: Tenant isolation (`client_id`) is enforced programmatically in `validateAgentSQL` + `requiresTenantFilter` from `@databuddy/db`. Never rely solely on system-prompt instructions for data isolation. All three SQL tool copies (API, RPC, private) must use the shared validation from `packages/db/src/clickhouse/sql-validation.ts`.
131+
- **Agent SQL security**: Tenant isolation (`client_id`) is enforced programmatically in `validateAgentSQL` + `requiresTenantFilter` from `@databuddy/db`. Never rely solely on system-prompt instructions for data isolation. Every SQL tool entry point (API, RPC, etc.) must use the shared validation from `packages/db/src/clickhouse/sql-validation.ts`.
132132
- **ClickHouse table allowlist**: Agent SQL is restricted to `analytics.*` tables only. `system.*`, `information_schema.*` are blocked. Add new allowed prefixes in `sql-validation.ts` if new databases are added.
133133
- **Flags API local dev** requires `dotenv -e .env` from repo root to pick up `REDIS_URL`, `DATABASE_URL`, etc.
134134
- **Node SDK flags**: The export is `createServerFlagsManager` (not `createFlagsManager`). Call `waitForInit()` before use.

apps/dashboard/components/ai-elements/ai-component.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,45 @@ import {
55
hasComponent,
66
type RawComponentInput,
77
} from "@/lib/ai-components";
8+
import { Skeleton } from "@/components/ui/skeleton";
9+
import { chartSurfaceClassName } from "@/lib/chart-presentation";
10+
11+
const SKELETON_LABELS: Record<string, string> = {
12+
"line-chart": "Loading chart...",
13+
"bar-chart": "Loading chart...",
14+
"area-chart": "Loading chart...",
15+
"stacked-bar-chart": "Loading chart...",
16+
"pie-chart": "Loading chart...",
17+
"donut-chart": "Loading chart...",
18+
"data-table": "Loading table...",
19+
"referrers-list": "Loading referrers...",
20+
"mini-map": "Loading map...",
21+
"links-list": "Loading links...",
22+
"link-preview": "Loading preview...",
23+
"funnels-list": "Loading funnels...",
24+
"funnel-preview": "Loading preview...",
25+
"goals-list": "Loading goals...",
26+
"goal-preview": "Loading preview...",
27+
"annotations-list": "Loading annotations...",
28+
"annotation-preview": "Loading preview...",
29+
};
30+
31+
function ComponentSkeleton({ type, title }: { type: string; title?: string }) {
32+
const label = SKELETON_LABELS[type] ?? "Loading...";
33+
return (
34+
<div className={chartSurfaceClassName}>
35+
<div className="dotted-bg bg-accent">
36+
<Skeleton className="h-[120px] w-full rounded-none" />
37+
</div>
38+
<div className="flex items-center gap-2.5 border-t px-3 py-2">
39+
<p className="min-w-0 flex-1 truncate text-muted-foreground text-sm">
40+
{title ?? label}
41+
</p>
42+
<div className="h-0.5 w-12 animate-pulse rounded bg-primary/30" />
43+
</div>
44+
</div>
45+
);
46+
}
847

948
interface AIComponentProps {
1049
input: RawComponentInput;
@@ -14,7 +53,8 @@ interface AIComponentProps {
1453

1554
/**
1655
* Renders an AI-generated component based on its type.
17-
* Looks up the component definition in the registry and renders it.
56+
* During streaming, skips strict validation and shows a skeleton
57+
* if the data is too incomplete to render.
1858
*/
1959
export function AIComponent({ input, className, streaming }: AIComponentProps) {
2060
if (!hasComponent(input.type)) {
@@ -26,12 +66,33 @@ export function AIComponent({ input, className, streaming }: AIComponentProps) {
2666
return null;
2767
}
2868

69+
// During streaming, try to render with whatever data we have.
70+
// If validation or transform fails, show a typed skeleton instead of nothing.
71+
if (streaming) {
72+
if (definition.validate(input)) {
73+
try {
74+
const props = definition.transform(input);
75+
const Component = definition.component;
76+
return <Component {...props} className={className} streaming />;
77+
} catch {
78+
// Transform failed on partial data — show skeleton
79+
}
80+
}
81+
return (
82+
<ComponentSkeleton
83+
type={input.type}
84+
title={typeof input.title === "string" ? input.title : undefined}
85+
/>
86+
);
87+
}
88+
89+
// Complete mode: strict validation
2990
if (!definition.validate(input)) {
3091
return null;
3192
}
3293

3394
const props = definition.transform(input);
3495
const Component = definition.component;
3596

36-
return <Component {...props} className={className} streaming={streaming} />;
97+
return <Component {...props} className={className} />;
3798
}

apps/dashboard/lib/ai-components/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** biome-ignore-all lint/performance/noBarrelFile: this is a barrel file */
22

3-
export { parseComponents, parseContentSegments } from "./parser";
3+
export { parseContentSegments } from "./parser";
44

55
export { componentRegistry, getComponent, hasComponent } from "./registry";
66

@@ -15,7 +15,6 @@ export type {
1515
DistributionInput,
1616
LinksListInput,
1717
MiniMapInput,
18-
ParsedContent,
1918
ParsedSegments,
2019
RawComponentInput,
2120
ReferrerItem,

apps/dashboard/lib/ai-components/parser.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { hasComponent } from "./registry";
22
import { validateComponentJSON } from "./schemas";
33
import type {
44
ContentSegment,
5-
ParsedContent,
65
ParsedSegments,
76
RawComponentInput,
87
} from "./types";
@@ -203,24 +202,3 @@ export function parseContentSegments(content: string): ParsedSegments {
203202
return { segments };
204203
}
205204

206-
/**
207-
* @deprecated Use parseContentSegments for ordered rendering
208-
* Parse component JSON objects from markdown content.
209-
* Extracts components in format: {"type":"..."}
210-
*/
211-
export function parseComponents(content: string): ParsedContent {
212-
const { segments } = parseContentSegments(content);
213-
214-
const text = segments
215-
.filter((s): s is ContentSegment & { type: "text" } => s.type === "text")
216-
.map((s) => s.content)
217-
.join(" ");
218-
219-
const components = segments
220-
.filter(
221-
(s): s is ContentSegment & { type: "component" } => s.type === "component"
222-
)
223-
.map((s) => s.content);
224-
225-
return { text, components };
226-
}

bun.lock

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)