Skip to content

Commit 4534152

Browse files
authored
Merge pull request #35 from datopian/feat/queryless-integration
feat: optional querylessai.com integration
2 parents 02fd9ad + bef3104 commit 4534152

File tree

9 files changed

+4898
-3336
lines changed

9 files changed

+4898
-3336
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ Then, you can start customizing it locally by following the development instruct
6767
```bash
6868
# This is the URL of the CKAN instance. Use the example value if you are using PortalJS Cloud.
6969
NEXT_PUBLIC_DMS=https://api.cloud.portaljs.com/@my-portal-main-org-name
70+
71+
# Optional Queryless AI assistant integration
72+
# Set to true to display the floating AI button + right drawer chat
73+
NEXT_PUBLIC_QUERYLESS_ENABLED=false
74+
75+
# Optional internal API route path used by the chat widget
76+
NEXT_PUBLIC_QUERYLESS_API_ROUTE=/api/queryless-chat
77+
78+
# Server-side Queryless API config (keep these non-public)
79+
QUERYLESS_URL=
80+
QUERYLESS_TOKEN=
81+
QUERYLESS_MODEL=agent:your-agent-id
7082
```
7183

7284
4) Run `npm run dev` to start the development server
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
ResponsiveContainer,
3+
CartesianGrid,
4+
XAxis,
5+
YAxis,
6+
Tooltip,
7+
Legend,
8+
BarChart,
9+
Bar,
10+
LineChart,
11+
Line,
12+
AreaChart,
13+
Area,
14+
PieChart,
15+
Pie,
16+
Cell,
17+
} from "recharts";
18+
import { z } from "zod";
19+
20+
const chartTypeSchema = z.enum(["bar", "line", "area", "pie", "table"]);
21+
const dataPointSchema = z.record(
22+
z.string(),
23+
z.union([z.string(), z.number(), z.null()])
24+
);
25+
26+
const chartSpecSchema = z.object({
27+
type: z.literal("chart"),
28+
chartType: chartTypeSchema,
29+
title: z.string().optional(),
30+
xKey: z.string().optional(),
31+
yKey: z.string().optional(),
32+
data: z.array(dataPointSchema).max(200),
33+
});
34+
35+
export type ChartSpec = z.infer<typeof chartSpecSchema>;
36+
37+
const CHART_COLORS = ["#0284c7", "#16a34a", "#f59e0b", "#ef4444", "#8b5cf6"];
38+
39+
export function parseChartSpec(value: unknown): ChartSpec | null {
40+
const parsed = chartSpecSchema.safeParse(value);
41+
if (!parsed.success) return null;
42+
return parsed.data;
43+
}
44+
45+
function toNumber(value: unknown): number {
46+
if (typeof value === "number") return value;
47+
if (typeof value === "string") {
48+
const n = Number(value);
49+
return Number.isFinite(n) ? n : 0;
50+
}
51+
return 0;
52+
}
53+
54+
export default function ChartRenderer({ chart }: { chart: ChartSpec }) {
55+
const xKey = chart.xKey || "x";
56+
const yKey = chart.yKey || "y";
57+
58+
if (chart.chartType === "table") {
59+
const columns = chart.data.length > 0 ? Object.keys(chart.data[0]) : [];
60+
return (
61+
<div className="mt-2 overflow-x-auto rounded border border-slate-200">
62+
<table className="w-full border-collapse text-xs">
63+
<thead className="bg-slate-100">
64+
<tr>
65+
{columns.map(column => (
66+
<th
67+
key={column}
68+
className="border border-slate-200 px-2 py-1 text-left font-semibold"
69+
>
70+
{column}
71+
</th>
72+
))}
73+
</tr>
74+
</thead>
75+
<tbody>
76+
{chart.data.map((row, index) => (
77+
<tr key={index}>
78+
{columns.map(column => (
79+
<td key={column} className="border border-slate-200 px-2 py-1">
80+
{String(row[column] ?? "")}
81+
</td>
82+
))}
83+
</tr>
84+
))}
85+
</tbody>
86+
</table>
87+
</div>
88+
);
89+
}
90+
91+
const normalizedData = chart.data.map(point => ({
92+
...point,
93+
[yKey]: toNumber(point[yKey]),
94+
}));
95+
96+
const commonMargin = { top: 8, right: 24, left: 12, bottom: 52 };
97+
const axisTick = { fontSize: 10 };
98+
99+
return (
100+
<div className="mt-2 h-72 w-full min-w-0 rounded border border-slate-200 bg-white p-2">
101+
<ResponsiveContainer width="100%" height="100%">
102+
<>
103+
{chart.chartType === "bar" && (
104+
<BarChart data={normalizedData} margin={commonMargin}>
105+
<CartesianGrid strokeDasharray="3 3" />
106+
<XAxis
107+
dataKey={xKey}
108+
interval={0}
109+
minTickGap={8}
110+
tick={axisTick}
111+
angle={-25}
112+
textAnchor="end"
113+
height={56}
114+
/>
115+
<YAxis width={56} tick={axisTick} />
116+
<Tooltip />
117+
<Legend />
118+
<Bar dataKey={yKey} fill="#0284c7" />
119+
</BarChart>
120+
)}
121+
{chart.chartType === "line" && (
122+
<LineChart data={normalizedData} margin={commonMargin}>
123+
<CartesianGrid strokeDasharray="3 3" />
124+
<XAxis
125+
dataKey={xKey}
126+
interval={0}
127+
minTickGap={8}
128+
tick={axisTick}
129+
angle={-25}
130+
textAnchor="end"
131+
height={56}
132+
/>
133+
<YAxis width={56} tick={axisTick} />
134+
<Tooltip />
135+
<Legend />
136+
<Line type="monotone" dataKey={yKey} stroke="#0284c7" strokeWidth={2} />
137+
</LineChart>
138+
)}
139+
{chart.chartType === "area" && (
140+
<AreaChart data={normalizedData} margin={commonMargin}>
141+
<CartesianGrid strokeDasharray="3 3" />
142+
<XAxis
143+
dataKey={xKey}
144+
interval={0}
145+
minTickGap={8}
146+
tick={axisTick}
147+
angle={-25}
148+
textAnchor="end"
149+
height={56}
150+
/>
151+
<YAxis width={56} tick={axisTick} />
152+
<Tooltip />
153+
<Legend />
154+
<Area type="monotone" dataKey={yKey} stroke="#0284c7" fill="#bae6fd" />
155+
</AreaChart>
156+
)}
157+
{chart.chartType === "pie" && (
158+
<PieChart>
159+
<Tooltip />
160+
<Legend />
161+
<Pie data={normalizedData} dataKey={yKey} nameKey={xKey} label>
162+
{normalizedData.map((_, index) => (
163+
<Cell key={index} fill={CHART_COLORS[index % CHART_COLORS.length]} />
164+
))}
165+
</Pie>
166+
</PieChart>
167+
)}
168+
</>
169+
</ResponsiveContainer>
170+
</div>
171+
);
172+
}

0 commit comments

Comments
 (0)