Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6e39297
feat: add @clickhouse/click-ui for prettier ClickHouse tool calls
dustinhealy Apr 13, 2026
b08d355
feat: redesign ClickHouse tool call with tabs, metrics, and structure…
dustinhealy Apr 13, 2026
4373020
feat: add wrapLines to all CodeBlocks, polish padding and layout
dustinhealy Apr 14, 2026
5d7390b
feat: collapsible service rows, flat tables, endpoint pills, CH desig…
dustinhealy Apr 14, 2026
5c7f830
fix: polish collapsible rows β€” radius, focus outlines, full-width sep…
dustinhealy Apr 14, 2026
56904ef
feat: add cost visualization, collapsible services, pass functionName
dustinhealy Apr 14, 2026
0308f2f
feat: static rows for service details, smart unwrap, error parsing, a…
dustinhealy Apr 14, 2026
3254583
fix: remove stray brace from JSX
dustinhealy Apr 14, 2026
08a0c0e
feat: special-case list_tables with collapsible rows and column type …
dustinhealy Apr 14, 2026
2883880
feat: render create_table_query and engine_full as SQL CodeBlocks
dustinhealy Apr 14, 2026
7e25386
feat: resizable columns, column filter, wrap mode, fade-in to prevent…
dustinhealy Apr 14, 2026
0adfe6b
feat: CheckboxMultiSelect for column filter, fix data unwrap, cost vi…
dustinhealy Apr 14, 2026
687f710
feat: virtualized Grid for query results with pagination
dustinhealy Apr 14, 2026
3009019
chore: formatting
dustinhealy Apr 14, 2026
1e1d0b9
refactor: organize ClickHouse into folder with types, helpers, and tests
dustinhealy Apr 14, 2026
f94d75b
style: run prettier and eslint fix on all ClickHouse files
dustinhealy Apr 14, 2026
961f85f
fix: add i18n keys for all ClickHouse UI strings, fix lint errors
dustinhealy Apr 14, 2026
30ae87f
fix: remove unused i18n plural key, use explicit row/rows keys
dustinhealy Apr 14, 2026
686ad20
chore: remove styled-components direct dependency, click-ui bundles i…
dustinhealy Apr 14, 2026
d7b8319
fix: restore click-ui chunk rule to keep vendor under PWA size limit
dustinhealy Apr 14, 2026
9b9818a
chore: bump PWA precache limit to 5MB for click-ui transitive deps
dustinhealy Apr 14, 2026
83038d7
fix: remove react-syntax-highlighter from markdown_highlight chunk, c…
dustinhealy Apr 14, 2026
98482a8
fix: isolate click-ui chunk to prevent React CJS interop breakage
dustinhealy Apr 15, 2026
92ad59b
feat: add syntax highlighting to CodeDisplay with Click UI color theme
dustinhealy Apr 15, 2026
1e12c29
fix: prettier formatting for chCodeStyles, add build notes
dustinhealy Apr 15, 2026
c19d181
feat: simplify CodeDisplay β€” always wrap, copy-only button, fix endpo…
dustinhealy Apr 15, 2026
5b71fe0
fix: address PR review feedback
dustinhealy Apr 15, 2026
8ea29a9
fix: prettier formatting
dustinhealy Apr 15, 2026
dacb25d
fix: address code review findings
dustinhealy Apr 15, 2026
b246ce5
fix: address remaining review findings
dustinhealy Apr 15, 2026
9594c18
fix: match CodeDisplay font/colors to Click UI CodeBlock theme tokens
dustinhealy Apr 15, 2026
043f63f
fix: guard getDateRange against all-malformed dates after filter
dustinhealy Apr 15, 2026
cfebb32
feat: idle service state, context pills, org routing, error detection
dustinhealy Apr 15, 2026
600f677
feat: add chart visualization with auto-detect and config picker
dustinhealy Apr 21, 2026
0883d02
fix: use recharts for chart rendering
dustinhealy Apr 21, 2026
1f80e32
fix: recharts dark mode colors and Vite bundling
dustinhealy Apr 21, 2026
e119054
fix: tooltip dark mode colors, labelColor init order, remove emojis
dustinhealy Apr 21, 2026
0c0baeb
refactor: use Click UI components and design tokens in chart config m…
dustinhealy Apr 21, 2026
fcf5fa1
fix: use Click UI Button for Apply/Cancel, proper theming
dustinhealy Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"homepage": "https://librechat.ai",
"dependencies": {
"@ariakit/react": "^0.4.15",
"@clickhouse/click-ui": "0.2.0-rc.4",
"recharts": "^2.15.0",
"@ariakit/react-core": "^0.4.17",
"@codesandbox/sandpack-react": "^2.19.10",
"@dicebear/collection": "^9.4.1",
Expand Down Expand Up @@ -85,11 +87,11 @@
"micromark-extension-llm-math": "^3.1.0",
"qrcode.react": "^4.2.0",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",
"react": "^18.3.1",
"react-avatar-editor": "^13.0.2",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-flip-toolkit": "^7.1.0",
"react-gtm-module": "^2.0.11",
"react-hook-form": "^7.43.9",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useState } from 'react';
import {
Button,
ButtonGroup,
Select,
TextField,
Panel,
Text,
Separator,
Container,
} from '@clickhouse/click-ui';
import type { ChartType, ChartConfig } from './types';
import { CHART_COLORS } from './types';
import { getColumnNames, getNumericColumns } from './chartDetect';

interface ChartConfigModalProps {
rows: Record<string, unknown>[];
initial?: ChartConfig;
onApply: (config: ChartConfig) => void;
onClose: () => void;
codeTheme: 'light' | 'dark';
}

const CHART_TYPE_OPTIONS = [
{ value: 'bar', label: 'Bar' },
{ value: 'hbar', label: 'H-Bar' },
{ value: 'sbar', label: 'Stacked' },
{ value: 'shbar', label: 'Stacked H' },
{ value: 'area', label: 'Area' },
{ value: 'line', label: 'Line' },
{ value: 'scatter', label: 'Scatter' },
{ value: 'pie', label: 'Pie' },
{ value: 'doughnut', label: 'Doughnut' },
];

export default function ChartConfigModal({
rows,
initial,
onApply,
onClose,
}: ChartConfigModalProps) {
const allColumns = getColumnNames(rows);
const numericColumns = getNumericColumns(rows);

const [chartType, setChartType] = useState<ChartType>(initial?.chartType ?? 'bar');
const [xAxis, setXAxis] = useState(initial?.xAxis ?? allColumns[0] ?? '');
const [yAxisCols, setYAxisCols] = useState<Set<string>>(
new Set(initial?.yAxis.map((y) => y.column) ?? (numericColumns.length > 0 ? [numericColumns[0]] : [])),
);
const [title, setTitle] = useState(initial?.title ?? '');

const handleApply = () => {
if (!xAxis || yAxisCols.size === 0) {
return;
}
onApply({
chartType,
xAxis,
yAxis: Array.from(yAxisCols).map((col, i) => ({
color: CHART_COLORS[i % CHART_COLORS.length],
column: col,
})),
title: title || undefined,
});
};

return (
<Panel padding="md" radii="sm" hasBorder orientation="vertical" fillWidth>
<Container orientation="horizontal" padding="none" justifyContent="space-between" alignItems="center">
<Text size="md" weight="bold">Chart Configuration</Text>
<button
type="button"
onClick={onClose}
style={{ color: 'var(--text-secondary)', background: 'none', border: 'none', cursor: 'pointer', fontSize: 16 }}
>
&times;
</button>
</Container>

<Separator size="xs" />

<Container orientation="vertical" padding="none" gap="sm">
<Text size="xs" color="muted" weight="medium">Chart Type</Text>
<ButtonGroup
options={CHART_TYPE_OPTIONS}
selected={chartType}
onClick={(value) => setChartType(value as ChartType)}
/>
</Container>

<Separator size="xs" />

<Container orientation="vertical" padding="none" gap="sm">
<Text size="xs" color="muted" weight="medium">X Axis</Text>
<Select value={xAxis} onSelect={(value) => setXAxis(value)}>
{allColumns.map((col) => (
<Select.Item key={col} value={col}>
{col}
</Select.Item>
))}
</Select>
</Container>

<Container orientation="vertical" padding="none" gap="sm">
<Text size="xs" color="muted" weight="medium">Y Axis</Text>
<ButtonGroup
options={allColumns
.filter((col) => col !== xAxis)
.map((col) => ({
value: col,
label: col,
disabled: !numericColumns.includes(col),
}))}
selected={yAxisCols}
onClick={(value, selected) => setYAxisCols(selected instanceof Set ? selected : new Set([selected]))}
multiple
/>
</Container>

<Separator size="xs" />

<Container orientation="vertical" padding="none" gap="sm">
<Text size="xs" color="muted" weight="medium">Title (optional)</Text>
<TextField
value={title}
onChange={(val) => setTitle(val)}
placeholder="Chart title..."
/>
</Container>

<Container orientation="horizontal" padding="none" gap="sm" justifyContent="end">
<Button type="secondary" label="Cancel" onClick={onClose} />
<Button type="primary" label="Apply" onClick={handleApply} disabled={!xAxis || yAxisCols.size === 0} />
</Container>
</Panel>
);
}
238 changes: 238 additions & 0 deletions client/src/components/Chat/Messages/Content/ClickHouse/ChartView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import React, { useMemo } from 'react';
import {
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
ScatterChart,
Scatter,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import type { ChartConfig } from './types';
import { CHART_COLORS } from './types';

interface ChartViewProps {
rows: Record<string, unknown>[];
chartConfig: ChartConfig;
codeTheme: 'light' | 'dark';
}

function toNumber(val: unknown): number {
if (typeof val === 'number') {
return val;
}
const n = Number(val);
return isNaN(n) ? 0 : n;
}

export default function ChartView({ rows, chartConfig, codeTheme }: ChartViewProps) {
const { chartType, xAxis, yAxis, title } = chartConfig;
const textColor = codeTheme === 'dark' ? '#b3b6bd' : '#696e79';
const labelColor = codeTheme === 'dark' ? '#ffffff' : '#1a1a1a';
const gridColor = codeTheme === 'dark' ? '#323232' : '#e6e7e9';
const tooltipBg = codeTheme === 'dark' ? '#282828' : '#ffffff';
const tooltipBorder = codeTheme === 'dark' ? '#3a3a3a' : '#e0e0e0';
const tooltipStyle: React.CSSProperties = { backgroundColor: tooltipBg, border: `1px solid ${tooltipBorder}`, fontSize: 12, borderRadius: 6, padding: '8px 12px' };
const tooltipLabelStyle: React.CSSProperties = { color: labelColor, fontWeight: 500, marginBottom: 4 };
const tooltipItemStyle: React.CSSProperties = { color: textColor };

// Transform rows into recharts-friendly format
const data = useMemo(
() =>
rows.map((row) => {
const point: Record<string, unknown> = { [xAxis]: row[xAxis] };
for (const y of yAxis) {
point[y.column] = toNumber(row[y.column]);
}
return point;
}),
[rows, xAxis, yAxis],
);

const isPie = chartType === 'pie' || chartType === 'doughnut';
const isHorizontal = chartType === 'hbar' || chartType === 'shbar';
const isStacked = chartType === 'sbar' || chartType === 'shbar';
const isScatter = chartType === 'scatter';
const isArea = chartType === 'area';
const isLine = chartType === 'line';

if (data.length === 0) {
return (
<div style={{ padding: 16, color: textColor }}>No data to chart</div>
);
}

// Pie / Doughnut
if (isPie) {
const valCol = yAxis[0]?.column;
if (!valCol) {
return null;
}
const pieData = data.map((d) => ({
name: String(d[xAxis] ?? ''),
value: toNumber(d[valCol]),
}));

return (
<div>
{title && (
<div style={{ textAlign: 'center', fontSize: 13, fontWeight: 500, color: textColor, marginBottom: 4 }}>
{title}
</div>
)}
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={pieData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
innerRadius={chartType === 'doughnut' ? 60 : 0}
outerRadius={100}
label={({ name, percent, x, y, textAnchor }) => (
<text x={x} y={y} textAnchor={textAnchor} fill={textColor} fontSize={11}>
{`${name} ${(percent * 100).toFixed(0)}%`}
</text>
)}
labelLine={false}
>
{pieData.map((_, i) => (
<Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
))}
</Pie>
<Tooltip contentStyle={tooltipStyle} labelStyle={tooltipLabelStyle} itemStyle={tooltipItemStyle} />
<Legend wrapperStyle={{ fontSize: 12, color: textColor }} />
</PieChart>
</ResponsiveContainer>
</div>
);
}

// Scatter
if (isScatter) {
const yCol = yAxis[0]?.column;
if (!yCol) {
return null;
}
const scatterData = data.map((d) => ({
x: toNumber(d[xAxis]),
y: toNumber(d[yCol]),
}));

return (
<div>
{title && (
<div style={{ textAlign: 'center', fontSize: 13, fontWeight: 500, color: textColor, marginBottom: 4 }}>
{title}
</div>
)}
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
<XAxis dataKey="x" name={xAxis} tick={{ fontSize: 11, fill: textColor }} />
<YAxis dataKey="y" name={yCol} tick={{ fontSize: 11, fill: textColor }} />
<Tooltip contentStyle={tooltipStyle} labelStyle={tooltipLabelStyle} itemStyle={tooltipItemStyle} cursor={{ strokeDasharray: '3 3' }} />
<Scatter data={scatterData} fill={yAxis[0]?.color ?? CHART_COLORS[0]} />
</ScatterChart>
</ResponsiveContainer>
</div>
);
}

// Bar (vertical / horizontal / stacked)
if (!isArea && !isLine) {
const ChartComponent = BarChart;
return (
<div>
{title && (
<div style={{ textAlign: 'center', fontSize: 13, fontWeight: 500, color: textColor, marginBottom: 4 }}>
{title}
</div>
)}
<ResponsiveContainer width="100%" height={300}>
<ChartComponent
data={data}
layout={isHorizontal ? 'vertical' : 'horizontal'}
>
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
{isHorizontal ? (
<>
<XAxis type="number" tick={{ fontSize: 11, fill: textColor }} />
<YAxis dataKey={xAxis} type="category" tick={{ fontSize: 11, fill: textColor }} width={100} />
</>
) : (
<>
<XAxis dataKey={xAxis} tick={{ fontSize: 11, fill: textColor }} />
<YAxis tick={{ fontSize: 11, fill: textColor }} />
</>
)}
<Tooltip contentStyle={tooltipStyle} labelStyle={tooltipLabelStyle} itemStyle={tooltipItemStyle} />
{yAxis.length > 1 && <Legend wrapperStyle={{ fontSize: 12, color: textColor }} />}
{yAxis.map((y, i) => (
<Bar
key={y.column}
dataKey={y.column}
fill={y.color || CHART_COLORS[i % CHART_COLORS.length]}
stackId={isStacked ? 'stack' : undefined}
maxBarSize={40}
/>
))}
</ChartComponent>
</ResponsiveContainer>
</div>
);
}

// Area / Line
const ChartComponent = isArea ? AreaChart : LineChart;
return (
<div>
{title && (
<div style={{ textAlign: 'center', fontSize: 13, fontWeight: 500, color: textColor, marginBottom: 4 }}>
{title}
</div>
)}
<ResponsiveContainer width="100%" height={300}>
<ChartComponent data={data}>
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
<XAxis dataKey={xAxis} tick={{ fontSize: 11, fill: textColor }} />
<YAxis tick={{ fontSize: 11, fill: textColor }} />
<Tooltip contentStyle={tooltipStyle} labelStyle={tooltipLabelStyle} itemStyle={tooltipItemStyle} />
{yAxis.length > 1 && <Legend wrapperStyle={{ fontSize: 12, color: textColor }} />}
{yAxis.map((y, i) => {
const color = y.color || CHART_COLORS[i % CHART_COLORS.length];
return isArea ? (
<Area
key={y.column}
type="monotone"
dataKey={y.column}
stroke={color}
fill={color}
fillOpacity={0.3}
/>
) : (
<Line
key={y.column}
type="monotone"
dataKey={y.column}
stroke={color}
dot={false}
/>
);
})}
</ChartComponent>
</ResponsiveContainer>
</div>
);
}
Loading
Loading