Skip to content

Commit 8050e9b

Browse files
committed
feat(queryless): simulate AI responses for home hero chips
Clicking one of the three suggested-question chips now shows the Thinking indicator for 2s and then reveals a pre-calculated answer (markdown + chart + method block) instead of calling the API. Typed questions still use the live streaming endpoint. Canned answers and the chip prompt list live in a single module so the chips and their responses stay in sync.
1 parent 3c9b8da commit 8050e9b

3 files changed

Lines changed: 171 additions & 7 deletions

File tree

src/app/[locale]/page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getAllGroups } from "@/lib/ckan/group";
1212
import { getAllReports } from "@/lib/reports";
1313
import { cn } from "@/lib/utils";
1414
import { isQuerylessEnabled } from "@/lib/queryless";
15+
import { QUERYLESS_SUGGESTED_PROMPTS } from "@/lib/queryless-canned";
1516
import { toPublicGroupSlug } from "@/lib/portal-name";
1617
import { ArrowRight, Code2, FolderKanban, Mail } from "lucide-react";
1718
import { Metadata } from "next";
@@ -40,11 +41,7 @@ export default async function Home() {
4041
const apiDocsUrl = "https://docs.ckan.org/en/2.11/api/index.html";
4142
const requestDataHref =
4243
"mailto:data@example.com?subject=Open%20Data%20Request";
43-
const suggestedPrompts = [
44-
"How has global temperature changed since 1980?",
45-
"Compare atmospheric CO2 trends since 2000",
46-
"Which countries spend most on pharmaceuticals?",
47-
];
44+
const suggestedPrompts = QUERYLESS_SUGGESTED_PROMPTS;
4845

4946
const [featuredDatasetsResult, statsResult, visualizationsResult, allGroups] =
5047
await Promise.all([

src/components/queryless/QuerylessAssistant.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
1919
import { CardTitle } from "@/components/ui/card";
2020
import { Textarea } from "@/components/ui/textarea";
2121
import { QUERYLESS_OPEN_EVENT, querylessConfig } from "@/lib/queryless";
22+
import { getCannedResponse } from "@/lib/queryless-canned";
2223
import { locales } from "@/i18n/config";
2324
import VegaSpecRenderer, { parseVegaSpecText } from "./VegaSpecRenderer";
2425

@@ -41,6 +42,9 @@ const QUERYLESS_DAILY_LIMIT_STORAGE_KEY = "queryless:daily-limit";
4142
const QUERYLESS_RATE_LIMIT_MAX_REQUESTS = 4;
4243
const QUERYLESS_RATE_LIMIT_WINDOW_MS = 60_000;
4344
const QUERYLESS_DAILY_LIMIT_MAX_REQUESTS = 20;
45+
// How long the "Thinking..." indicator shows before a pre-calculated
46+
// (simulated) answer is revealed for the home-page suggested-question chips.
47+
const QUERYLESS_SIMULATED_THINKING_MS = 2000;
4448

4549
function createSessionId() {
4650
return `queryless-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
@@ -526,6 +530,7 @@ export default function QuerylessAssistant() {
526530
const previousMessageCountRef = useRef(0);
527531
const shouldAutoScrollRef = useRef(true);
528532
const streamRafRef = useRef<number | null>(null);
533+
const simulateTimeoutRef = useRef<number | null>(null);
529534
const streamedAnswerRef = useRef("");
530535
const sessionIdRef = useRef<string>(createSessionId());
531536
const lastContextPathRef = useRef<string | null>(null);
@@ -783,6 +788,41 @@ export default function QuerylessAssistant() {
783788
}
784789
}, [context.pageDirective, context.path, input, isSending, messages, scrollMessagesToBottom]);
785790

791+
// Simulate an AI response for the home-page suggested-question chips: show the
792+
// user's question, display the "Thinking..." indicator for a short delay, then
793+
// reveal a pre-calculated answer. No API request is made and usage limits are
794+
// not touched, since nothing is actually sent upstream.
795+
const simulateMessage = useCallback(
796+
(question: string, answer: string) => {
797+
const trimmed = question.trim();
798+
if (!trimmed || isSending) return;
799+
800+
hasExchangeSinceLastPageChangeRef.current = true;
801+
shouldAutoScrollRef.current = true;
802+
const assistantMessageId = `assistant-${Date.now()}`;
803+
804+
setMessages((prev) => [
805+
...prev,
806+
{ id: `user-${Date.now()}`, role: "user", content: trimmed },
807+
{ id: assistantMessageId, role: "assistant", content: "" },
808+
]);
809+
setInput("");
810+
setError(null);
811+
setIsSending(true);
812+
813+
simulateTimeoutRef.current = window.setTimeout(() => {
814+
setMessages((prev) =>
815+
prev.map((message) =>
816+
message.id === assistantMessageId ? { ...message, content: answer } : message
817+
)
818+
);
819+
setIsSending(false);
820+
simulateTimeoutRef.current = null;
821+
}, QUERYLESS_SIMULATED_THINKING_MS);
822+
},
823+
[isSending]
824+
);
825+
786826
useEffect(() => {
787827
const handleOpenQueryless = (event: Event) => {
788828
const customEvent = event as CustomEvent<{
@@ -799,8 +839,13 @@ export default function QuerylessAssistant() {
799839
customEvent.preventDefault();
800840

801841
if (customEvent.detail?.autoSubmit && prompt) {
842+
const cannedAnswer = getCannedResponse(prompt);
802843
window.setTimeout(() => {
803-
void sendMessage(prompt);
844+
if (cannedAnswer) {
845+
simulateMessage(prompt, cannedAnswer);
846+
} else {
847+
void sendMessage(prompt);
848+
}
804849
}, 0);
805850
}
806851
};
@@ -809,7 +854,7 @@ export default function QuerylessAssistant() {
809854
return () => {
810855
window.removeEventListener(QUERYLESS_OPEN_EVENT, handleOpenQueryless as EventListener);
811856
};
812-
}, [sendMessage]);
857+
}, [sendMessage, simulateMessage]);
813858

814859
useEffect(() => {
815860
if (!rateLimitRetryAt && !rateLimitMessage) return;
@@ -983,6 +1028,10 @@ export default function QuerylessAssistant() {
9831028
window.cancelAnimationFrame(streamRafRef.current);
9841029
streamRafRef.current = null;
9851030
}
1031+
if (simulateTimeoutRef.current !== null) {
1032+
window.clearTimeout(simulateTimeoutRef.current);
1033+
simulateTimeoutRef.current = null;
1034+
}
9861035
},
9871036
[]
9881037
);

src/lib/queryless-canned.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Pre-calculated Queryless answers used to simulate an AI response for the
2+
// home-page suggested-question chips. Clicking one of these chips opens the
3+
// assistant, shows the "Thinking..." indicator for a moment, and then renders
4+
// the matching canned answer below — no real API request is made.
5+
//
6+
// The map is keyed by the exact prompt text shown on the chip, so
7+
// QUERYLESS_SUGGESTED_PROMPTS is the single source of truth: the home page
8+
// renders these strings as chips and the assistant looks the answer up by the
9+
// same string.
10+
11+
const TEMPERATURE_ANSWER = `Global surface temperatures have warmed markedly since 1980. Compared with the 20th-century average, the annual temperature anomaly has risen from roughly **+0.3 °C** in 1980 to about **+1.2 °C** in 2023 — a gain of nearly a full degree in just over four decades, with the steepest warming after 2000.
12+
13+
\`\`\`chart
14+
{
15+
"title": "Global temperature anomaly since 1980",
16+
"mark": {"type": "line", "point": true, "color": "#e11d48"},
17+
"data": {
18+
"values": [
19+
{"year": 1980, "anomaly": 0.28},
20+
{"year": 1985, "anomaly": 0.14},
21+
{"year": 1990, "anomaly": 0.45},
22+
{"year": 1995, "anomaly": 0.46},
23+
{"year": 2000, "anomaly": 0.42},
24+
{"year": 2005, "anomaly": 0.69},
25+
{"year": 2010, "anomaly": 0.72},
26+
{"year": 2015, "anomaly": 0.90},
27+
{"year": 2020, "anomaly": 1.02},
28+
{"year": 2023, "anomaly": 1.17}
29+
]
30+
},
31+
"encoding": {
32+
"x": {"field": "year", "type": "quantitative", "title": "Year", "axis": {"format": "d"}},
33+
"y": {"field": "anomaly", "type": "quantitative", "title": "Temp anomaly (°C)"}
34+
}
35+
}
36+
\`\`\`
37+
38+
The warming is not steady year to year, but the long-term trend is unmistakable and accelerating.
39+
40+
[[QUERYLESS_METHOD]]
41+
Values are global land–ocean temperature anomalies relative to the 1901–2000 mean, sampled at five-year intervals (plus the latest year). Figures are illustrative of the widely reported GISTEMP/HadCRUT records.
42+
[[/QUERYLESS_METHOD]]`;
43+
44+
const CO2_ANSWER = `Atmospheric CO₂ has risen steadily since 2000. Measured at Mauna Loa, the annual mean climbed from about **369 ppm** in 2000 to roughly **421 ppm** in 2023 — an increase of more than **50 ppm** (about 14%) in a little over two decades, with the yearly rise itself getting faster.
45+
46+
\`\`\`chart
47+
{
48+
"title": "Atmospheric CO₂ concentration since 2000 (ppm)",
49+
"mark": {"type": "line", "point": true, "color": "#0f766e"},
50+
"data": {
51+
"values": [
52+
{"year": 2000, "ppm": 369.5},
53+
{"year": 2003, "ppm": 375.8},
54+
{"year": 2006, "ppm": 381.9},
55+
{"year": 2009, "ppm": 387.4},
56+
{"year": 2012, "ppm": 393.8},
57+
{"year": 2015, "ppm": 400.8},
58+
{"year": 2018, "ppm": 408.5},
59+
{"year": 2021, "ppm": 416.4},
60+
{"year": 2023, "ppm": 421.1}
61+
]
62+
},
63+
"encoding": {
64+
"x": {"field": "year", "type": "quantitative", "title": "Year", "axis": {"format": "d"}},
65+
"y": {"field": "ppm", "type": "quantitative", "title": "CO₂ (ppm)", "scale": {"zero": false}}
66+
}
67+
}
68+
\`\`\`
69+
70+
The trend line is remarkably smooth — annual growth has averaged around 2 ppm per year and shows no sign of slowing.
71+
72+
[[QUERYLESS_METHOD]]
73+
Values are annual mean atmospheric CO₂ concentrations in parts per million, in the style of the NOAA Mauna Loa (Keeling Curve) record, sampled at three-year intervals plus the latest year. Figures are illustrative.
74+
[[/QUERYLESS_METHOD]]`;
75+
76+
const PHARMA_ANSWER = `Pharmaceutical spending per capita is highest in the United States by a wide margin, followed by other high-income economies. On a per-person basis, the **United States** spends roughly **$1,430 per year** on pharmaceuticals — well ahead of Switzerland, Germany and Japan.
77+
78+
\`\`\`chart
79+
{
80+
"title": "Pharmaceutical spending per capita (USD/year)",
81+
"mark": {"type": "bar", "color": "#4f46e5"},
82+
"data": {
83+
"values": [
84+
{"country": "United States", "spend": 1432},
85+
{"country": "Switzerland", "spend": 963},
86+
{"country": "Germany", "spend": 884},
87+
{"country": "Japan", "spend": 817},
88+
{"country": "Canada", "spend": 790},
89+
{"country": "France", "spend": 646},
90+
{"country": "Italy", "spend": 592},
91+
{"country": "United Kingdom", "spend": 497}
92+
]
93+
},
94+
"encoding": {
95+
"y": {"field": "country", "type": "nominal", "title": null, "sort": "-x"},
96+
"x": {"field": "spend", "type": "quantitative", "title": "USD per capita"}
97+
}
98+
}
99+
\`\`\`
100+
101+
The gap between the US and the next-highest spenders is striking — reflecting both higher drug prices and higher utilisation.
102+
103+
[[QUERYLESS_METHOD]]
104+
Values are illustrative annual pharmaceutical expenditure per capita in US dollars, in the style of OECD Health Statistics. Countries are sorted from highest to lowest spend.
105+
[[/QUERYLESS_METHOD]]`;
106+
107+
export const QUERYLESS_CANNED_RESPONSES: Record<string, string> = {
108+
"How has global temperature changed since 1980?": TEMPERATURE_ANSWER,
109+
"Compare atmospheric CO2 trends since 2000": CO2_ANSWER,
110+
"Which countries spend most on pharmaceuticals?": PHARMA_ANSWER,
111+
};
112+
113+
// Order matters: this is the exact list rendered as home-page chips.
114+
export const QUERYLESS_SUGGESTED_PROMPTS = Object.keys(QUERYLESS_CANNED_RESPONSES);
115+
116+
export function getCannedResponse(prompt: string): string | null {
117+
return QUERYLESS_CANNED_RESPONSES[prompt.trim()] ?? null;
118+
}

0 commit comments

Comments
 (0)