Skip to content

Commit 4ca5654

Browse files
committed
fix(clickhouse-chat-agent): address review feedback
- Keep data-derived keys out of the injected chart stylesheet: chart configs carry labels only (colors are applied directly on the marks), and ChartStyle sanitizes keys before interpolating CSS var names - Wrap the json-render Renderer in an error boundary so a bad spec degrades to an inline message instead of crashing the chat - Nested optional catalog fields (series labels, map point labels/values) use .nullish() so the model can omit them, as the prompt says it may - Validate longitude range alongside latitude in PointMap - Fix Tailwind v4 CSS-variable shorthand on the tooltip indicator
1 parent b1022c7 commit 4ca5654

5 files changed

Lines changed: 49 additions & 13 deletions

File tree

‎clickhouse-chat-agent/src/components/charts.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ function seriesColor(index: number): string {
3333
return `var(--chart-${(index % 5) + 1})`;
3434
}
3535

36+
// Config carries labels only. Colors are applied directly on the marks
37+
// (Bar/Line/Cell fills) — keeping model/data-derived keys out of the
38+
// stylesheet that ChartStyle would otherwise inject them into.
3639
function buildConfig(series: Series[]): ChartConfig {
37-
return Object.fromEntries(
38-
series.map((s, i) => [s.dataKey, { label: s.label ?? s.dataKey, color: seriesColor(i) }])
39-
);
40+
return Object.fromEntries(series.map((s) => [s.dataKey, { label: s.label ?? s.dataKey }]));
4041
}
4142

4243
function ChartFrame({ title, children }: { title?: string | null; children: React.ReactNode }) {
@@ -176,7 +177,7 @@ export function PieChartView({
176177
title?: string | null;
177178
}) {
178179
const config: ChartConfig = Object.fromEntries(
179-
data.map((row, i) => [String(row[nameKey]), { label: String(row[nameKey]), color: seriesColor(i) }])
180+
data.map((row) => [String(row[nameKey]), { label: String(row[nameKey]) }])
180181
);
181182

182183
return (

‎clickhouse-chat-agent/src/components/point-map.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ const MAX_SIZE = 30;
1616

1717
export function PointMapView({ points, title }: { points: Point[]; title?: string | null }) {
1818
const valid = points.filter(
19-
(p) => Number.isFinite(p.lat) && Number.isFinite(p.lng) && Math.abs(p.lat) <= 90
19+
(p) =>
20+
Number.isFinite(p.lat) &&
21+
Number.isFinite(p.lng) &&
22+
Math.abs(p.lat) <= 90 &&
23+
Math.abs(p.lng) <= 180
2024
);
2125
if (valid.length === 0) {
2226
return <div className="text-sm text-muted-foreground">No mappable points.</div>;

‎clickhouse-chat-agent/src/components/ui/chart.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ ${colorConfig
8888
const color =
8989
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
9090
itemConfig.color
91-
return color ? ` --color-${key}: ${color};` : null
91+
// Config keys can be data-derived — strip anything that could break out
92+
// of the stylesheet before interpolating into the CSS var name.
93+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_")
94+
return color ? ` --color-${safeKey}: ${color};` : null
9295
})
9396
.join("\n")}
9497
}
@@ -208,7 +211,7 @@ const ChartTooltipContent = React.forwardRef<
208211
!hideIndicator && (
209212
<div
210213
className={cn(
211-
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
214+
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
212215
{
213216
"h-2.5 w-2.5": indicator === "dot",
214217
"w-1": indicator === "line",
Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,40 @@
11
"use client";
22

33
import { JSONUIProvider, Renderer } from "@json-render/react";
4+
import { Component, type ReactNode } from "react";
45
import type { VisualizationSpec } from "@/lib/catalog";
56
import { registry } from "@/lib/registry";
67

8+
// Specs render as soon as they finish streaming — before the tool's
9+
// validation result lands — so a bad spec must degrade to an inline
10+
// message rather than crash the chat.
11+
class VisualizationErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
12+
state = { failed: false };
13+
14+
static getDerivedStateFromError() {
15+
return { failed: true };
16+
}
17+
18+
render() {
19+
if (this.state.failed) {
20+
return (
21+
<div className="my-3 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground">
22+
Couldn&apos;t render this visualization.
23+
</div>
24+
);
25+
}
26+
return this.props.children;
27+
}
28+
}
29+
730
export function Visualization({ spec }: { spec: VisualizationSpec }) {
831
return (
932
<div className="my-3">
10-
<JSONUIProvider registry={registry}>
11-
<Renderer spec={spec} registry={registry} />
12-
</JSONUIProvider>
33+
<VisualizationErrorBoundary>
34+
<JSONUIProvider registry={registry}>
35+
<Renderer spec={spec} registry={registry} />
36+
</JSONUIProvider>
37+
</VisualizationErrorBoundary>
1338
</div>
1439
);
1540
}

‎clickhouse-chat-agent/src/lib/catalog.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ const chartData = z
1515
.array(z.record(z.string(), z.union([z.string(), z.number(), z.null()])))
1616
.describe("Data rows, one object per x-axis entry");
1717

18+
// Nested optional fields use .nullish() (not .nullable()) so the model can
19+
// omit them: the top-level null-fill in validateSpec doesn't recurse into
20+
// arrays, and bare .nullable() rejects a missing key.
1821
const series = z
1922
.array(
2023
z.object({
2124
dataKey: z.string().describe("Key in each data row holding this series' numeric value"),
22-
label: z.string().nullable().describe("Human-readable series name for legend/tooltip"),
25+
label: z.string().nullish().describe("Human-readable series name for legend/tooltip"),
2326
})
2427
)
2528
.describe("One entry per plotted series");
@@ -74,10 +77,10 @@ export const chartComponentDefinitions = {
7477
z.object({
7578
lat: z.number(),
7679
lng: z.number(),
77-
label: z.string().nullable().describe("Shown in the marker tooltip"),
80+
label: z.string().nullish().describe("Shown in the marker tooltip"),
7881
value: z
7982
.number()
80-
.nullable()
83+
.nullish()
8184
.describe("Optional magnitude — scales the marker size and shows in the tooltip"),
8285
})
8386
)

0 commit comments

Comments
 (0)