From 03f3d26eed8496066ccc19e6c7f0afad328415b1 Mon Sep 17 00:00:00 2001 From: Scott Agrimson Date: Tue, 30 Jun 2026 15:12:09 -0700 Subject: [PATCH 01/18] pass user selection to prompt for followups --- .../scopes/atlas/query_provider.tsx | 19 +++++++++++++++++-- .../src/functions/build_follow_up_prompt.ts | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 dataweaver/apps/web/src/functions/build_follow_up_prompt.ts diff --git a/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx b/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx index c2d9f72e..9578baff 100644 --- a/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx +++ b/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx @@ -9,6 +9,7 @@ import { useRef, } from 'react'; import { toast } from '~/components/foundations/toaster/store'; +import { buildFollowUpPrompt } from '~/functions/build_follow_up_prompt'; import type { CardEntry, StreamEvent } from '~/server/types'; import { STATUS, STREAM_EVENT } from '~/server/types'; import { useAtlasStore } from '~/store'; @@ -162,6 +163,7 @@ export const QueryProvider = ({ children }: QueryProviderProps) => { () => ({ runPrompt: (prompt: string) => { const { + nodes, queryStart, querySetProcessing, querySetStatus, @@ -181,11 +183,24 @@ export const QueryProvider = ({ children }: QueryProviderProps) => { places: node.parsedQuery?.places ?? [], })); + // If the parent node had a followUp, enrich the prompt with the + // original query so the model has full context for the selection. + let resolvedPrompt = prompt; + if (parentNodeId) { + const parentNode = nodes[parentNodeId]; + const parentHadFollowUp = parentNode?.results + ? Object.values(parentNode.results).some((r) => r.followUp) + : false; + if (parentHadFollowUp && parentNode) { + resolvedPrompt = buildFollowUpPrompt(parentNode.query, prompt); + } + } + // Derive selected entity dcids from selected cards via store const selectedEntityDcids = getSelectedEntityDcids(selectedShapeIds); // Create history node in store - const nodeId = queryStart(prompt, null, parentNodeId); + const nodeId = queryStart(resolvedPrompt, null, parentNodeId); // Store active query state for the stream handler activeQueryRef.current = { @@ -201,7 +216,7 @@ export const QueryProvider = ({ children }: QueryProviderProps) => { // Start the SSE stream startStream({ - query: prompt, + query: resolvedPrompt, atlasContext, ancestorChain, selectedEntityDcids, diff --git a/dataweaver/apps/web/src/functions/build_follow_up_prompt.ts b/dataweaver/apps/web/src/functions/build_follow_up_prompt.ts new file mode 100644 index 00000000..f03c169c --- /dev/null +++ b/dataweaver/apps/web/src/functions/build_follow_up_prompt.ts @@ -0,0 +1,18 @@ +/** + * Constructs a composite prompt when a user selects a followUp option, + * combining the original query with the selected label so the model + * has enough context to return actual data. + */ +export function buildFollowUpPrompt( + originalQuery: string, + selection: string, +): string { + // Strategy 1: Concatenation + return `${originalQuery}. ${selection}`; + + // // Strategy 2: Natural sentence + // return `${originalQuery}, specifically ${selection}`; + + // // Strategy 3: Structured (keeps original and selection as distinct parts) + // return `Original question: ${originalQuery}\nUser selected: ${selection}`; +} From d5e6188e4397d2f1d86f12cc1488abbf6a0be436 Mon Sep 17 00:00:00 2001 From: Scott Agrimson Date: Tue, 30 Jun 2026 16:28:29 -0700 Subject: [PATCH 02/18] refactor to add followup context with original prompt and array of followups to send to api --- .../apps/web/src/app/api/query/route.ts | 17 +++++-- .../scopes/atlas/query_provider.tsx | 45 ++++++++++++++----- .../src/functions/build_follow_up_prompt.ts | 18 -------- .../src/server/config/skills/parse_query.md | 2 +- .../web/src/server/steps/data_discovery.ts | 27 ++++++++++- .../apps/web/src/server/steps/parse_query.ts | 23 +++++++--- dataweaver/apps/web/src/server/types.ts | 12 +++++ dataweaver/apps/web/src/store/store.ts | 5 ++- 8 files changed, 106 insertions(+), 43 deletions(-) delete mode 100644 dataweaver/apps/web/src/functions/build_follow_up_prompt.ts diff --git a/dataweaver/apps/web/src/app/api/query/route.ts b/dataweaver/apps/web/src/app/api/query/route.ts index 229398e2..e38c7c1a 100644 --- a/dataweaver/apps/web/src/app/api/query/route.ts +++ b/dataweaver/apps/web/src/app/api/query/route.ts @@ -51,6 +51,7 @@ export async function POST(request: NextRequest) { const selectedEntityDcids = Array.isArray(body?.selectedEntityDcids) ? body.selectedEntityDcids : []; + const followUpContext = body?.followUpContext ?? undefined; if (typeof query !== 'string' || !query.trim()) { return new Response(JSON.stringify({ error: 'Missing or invalid query' }), { @@ -95,16 +96,16 @@ export async function POST(request: NextRequest) { query, atlasContext, ancestorChainLength: ancestorChain.length, + followUpContext, }); // Resolve places let places = parsed.places; if (parsed.isFollowUp && places.length === 0) { - places = - selectedEntityDcids.length > 0 ? selectedEntityDcids : [query]; + places = selectedEntityDcids.length > 0 ? selectedEntityDcids : []; } - if (places.length === 0) places = [query]; - parsed.places = places; + const noExplicitPlace = places.length === 0; + if (noExplicitPlace) places = ['Earth']; emit({ type: STREAM_EVENT.parsedQuery, data: parsed }); if (signal.aborted) { @@ -152,6 +153,8 @@ export async function POST(request: NextRequest) { parsed, atlasContext, ancestorChain, + followUpContext, + noExplicitPlace, geminiTools, signal, onToolCall: (e) => { @@ -249,6 +252,12 @@ export async function POST(request: NextRequest) { followUp: parsedResponse.followUp, }; + // Strip follow-up options when no explicit place was provided — + // the question should only ask the user to specify a place. + if (noExplicitPlace && discoveryResult.followUp) { + discoveryResult.followUp.options = []; + } + const { tableHtml, notesHtml } = renderResultHtml(discoveryResult); discoveryResult.tableHtml = tableHtml; discoveryResult.notesHtml = notesHtml; diff --git a/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx b/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx index 9578baff..28acb789 100644 --- a/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx +++ b/dataweaver/apps/web/src/components/scopes/atlas/query_provider.tsx @@ -9,8 +9,8 @@ import { useRef, } from 'react'; import { toast } from '~/components/foundations/toaster/store'; -import { buildFollowUpPrompt } from '~/functions/build_follow_up_prompt'; -import type { CardEntry, StreamEvent } from '~/server/types'; + +import type { CardEntry, FollowUpContext, StreamEvent } from '~/server/types'; import { STATUS, STREAM_EVENT } from '~/server/types'; import { useAtlasStore } from '~/store'; import { useAtlas } from './atlas_provider'; @@ -183,16 +183,36 @@ export const QueryProvider = ({ children }: QueryProviderProps) => { places: node.parsedQuery?.places ?? [], })); - // If the parent node had a followUp, enrich the prompt with the - // original query so the model has full context for the selection. - let resolvedPrompt = prompt; + // If the parent node had a followUp, build structured context + // instead of concatenating into the query string. + let followUpContext: FollowUpContext | undefined; if (parentNodeId) { const parentNode = nodes[parentNodeId]; - const parentHadFollowUp = parentNode?.results - ? Object.values(parentNode.results).some((r) => r.followUp) - : false; - if (parentHadFollowUp && parentNode) { - resolvedPrompt = buildFollowUpPrompt(parentNode.query, prompt); + const parentFollowUp = parentNode?.results + ? Object.values(parentNode.results) + .map((r) => r.followUp) + .find(Boolean) + : undefined; + + if (parentFollowUp && parentNode) { + if (parentNode.followUpContext) { + // Extend existing chain + followUpContext = { + originalQuery: parentNode.followUpContext.originalQuery, + followUps: [ + ...parentNode.followUpContext.followUps, + { question: parentFollowUp.question, answer: prompt }, + ], + }; + } else { + // Start a new chain + followUpContext = { + originalQuery: parentNode.query, + followUps: [ + { question: parentFollowUp.question, answer: prompt }, + ], + }; + } } } @@ -200,7 +220,7 @@ export const QueryProvider = ({ children }: QueryProviderProps) => { const selectedEntityDcids = getSelectedEntityDcids(selectedShapeIds); // Create history node in store - const nodeId = queryStart(resolvedPrompt, null, parentNodeId); + const nodeId = queryStart(prompt, null, parentNodeId, followUpContext); // Store active query state for the stream handler activeQueryRef.current = { @@ -216,10 +236,11 @@ export const QueryProvider = ({ children }: QueryProviderProps) => { // Start the SSE stream startStream({ - query: resolvedPrompt, + query: prompt, atlasContext, ancestorChain, selectedEntityDcids, + followUpContext, }); }, queryCancel: () => { diff --git a/dataweaver/apps/web/src/functions/build_follow_up_prompt.ts b/dataweaver/apps/web/src/functions/build_follow_up_prompt.ts deleted file mode 100644 index f03c169c..00000000 --- a/dataweaver/apps/web/src/functions/build_follow_up_prompt.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Constructs a composite prompt when a user selects a followUp option, - * combining the original query with the selected label so the model - * has enough context to return actual data. - */ -export function buildFollowUpPrompt( - originalQuery: string, - selection: string, -): string { - // Strategy 1: Concatenation - return `${originalQuery}. ${selection}`; - - // // Strategy 2: Natural sentence - // return `${originalQuery}, specifically ${selection}`; - - // // Strategy 3: Structured (keeps original and selection as distinct parts) - // return `Original question: ${originalQuery}\nUser selected: ${selection}`; -} diff --git a/dataweaver/apps/web/src/server/config/skills/parse_query.md b/dataweaver/apps/web/src/server/config/skills/parse_query.md index 6840ebac..25cf1399 100644 --- a/dataweaver/apps/web/src/server/config/skills/parse_query.md +++ b/dataweaver/apps/web/src/server/config/skills/parse_query.md @@ -5,7 +5,7 @@ description: Parses a user query into structured fields (places, topic, date ran You are a query semantic analyzer for a statistical data exploration tool. Read the user's query and return a JSON object with exactly these fields: -- **places**: a JSON array of specific place names or DCIDs (resolve acronyms like "BRIC" → ["Brazil", "Russia", "India", "China"]). If the query is a follow-up that doesn't mention new places, return an empty array `[]`. +- **places**: a JSON array of specific place names or DCIDs (resolve acronyms like "BRIC" → ["Brazil", "Russia", "India", "China"]). If the query is a follow-up, extract places from the full conversation context (original query + clarifications). Only return an empty array if no places are mentioned anywhere in the conversation. - **topic**: a short string describing the statistical topic being asked about (e.g., "economy", "unemployment", "job market"). - **titles**: a JSON object mapping each extracted place to a short, human-like title for visual cards (e.g. "Economic landscape of France"). The title MUST speak to the topic but strictly reference ONLY that specific location. - **isFollowUp**: boolean — `true` if this query references previous context, asks to compare, refine, or build on prior results (e.g. "what about France?", "compare that with India"). `false` if it's a completely new standalone question. diff --git a/dataweaver/apps/web/src/server/steps/data_discovery.ts b/dataweaver/apps/web/src/server/steps/data_discovery.ts index 334cc68d..6af5ce7c 100644 --- a/dataweaver/apps/web/src/server/steps/data_discovery.ts +++ b/dataweaver/apps/web/src/server/steps/data_discovery.ts @@ -3,6 +3,7 @@ import { getGenAI } from '~/server/clients/gemini'; import { callMcp } from '~/server/clients/mcp'; import { getServiceConfig, getSkillConfig } from '~/server/config'; import type { + FollowUpContext, McpToolCallResult, McpToolsListResult, ParsedQuery, @@ -31,6 +32,9 @@ interface ToolLoopParams { parsed: ParsedQuery; atlasContext: string; ancestorChain: { query: string; topic: string; places: string[] }[]; + followUpContext?: FollowUpContext; + /** When true, no specific place was mentioned — 'Earth' is used as a default. */ + noExplicitPlace?: boolean; geminiTools: GeminiTool[]; signal?: AbortSignal; onToolCall?: (event: ToolCallEvent) => void; @@ -73,6 +77,7 @@ export const runToolLoop = async ( parsed, atlasContext, ancestorChain, + followUpContext, geminiTools, signal, onToolCall, @@ -95,13 +100,31 @@ export const runToolLoop = async ( ? `\n\nDATE CONSTRAINT: The user wants data limited to ${parsed.dateRange.start && parsed.dateRange.end ? `${parsed.dateRange.start} through ${parsed.dateRange.end}` : parsed.dateRange.start ? `from ${parsed.dateRange.start} onward` : `up to ${parsed.dateRange.end}`}.` : ''; - const placeClause = `\n\nTARGET PLACE: "${place}" — all variables MUST be relevant to this location.`; + const placeClause = params.noExplicitPlace + ? `\n\nTARGET PLACE: The user did not mention a specific place. Default to "Earth" (world-level) to retrieve initial data. You MUST include a followUp in your response asking which specific place or region the user would like to explore. The followUp MUST have an empty "options" array — do NOT suggest options, only provide a summary and question.` + : `\n\nTARGET PLACE: "${place}" — all variables MUST be relevant to this location.`; const systemInstruction = skill.systemPrompt + atlasClause + dateRangeClause + placeClause; // Build messages with conversation history const messages: Content[] = []; - if (ancestorChain.length > 0) { + if (followUpContext) { + // Explicit follow-up disambiguation context takes priority + const chain = followUpContext.followUps + .map( + (f, i) => + `Clarification ${i + 1}: "${f.question}" → User answered: "${f.answer}"`, + ) + .join('\n'); + messages.push({ + role: 'user', + parts: [ + { + text: `FOLLOW-UP CONTEXT:\nOriginal question: "${followUpContext.originalQuery}"\n${chain}\n\nNEW REQUEST: Find statistical variables for "${place}" related to: "${query}"`, + }, + ], + }); + } else if (ancestorChain.length > 0) { const historyContext = ancestorChain .map( (node) => diff --git a/dataweaver/apps/web/src/server/steps/parse_query.ts b/dataweaver/apps/web/src/server/steps/parse_query.ts index 50d79c6f..5d0d40c8 100644 --- a/dataweaver/apps/web/src/server/steps/parse_query.ts +++ b/dataweaver/apps/web/src/server/steps/parse_query.ts @@ -1,12 +1,13 @@ import { extractJson } from '~/functions/extract_json'; import { getGenAI } from '~/server/clients/gemini'; import { getServiceConfig, getSkillConfig } from '~/server/config'; -import type { ParsedQuery } from '~/server/types'; +import type { FollowUpContext, ParsedQuery } from '~/server/types'; interface ParseQueryParams { query: string; atlasContext: string; ancestorChainLength: number; + followUpContext?: FollowUpContext; } /** @@ -16,7 +17,7 @@ interface ParseQueryParams { export const parseQuery = async ( params: ParseQueryParams, ): Promise => { - const { query, atlasContext, ancestorChainLength } = params; + const { query, atlasContext, ancestorChainLength, followUpContext } = params; const config = getServiceConfig(); const skill = getSkillConfig('parse_query'); const genAI = getGenAI(); @@ -28,9 +29,21 @@ export const parseQuery = async ( const atlasHint = atlasContext ? `\nAtlas context: ${atlasContext}` : ''; const systemPrompt = skill.systemPrompt + historyHint + atlasHint; + // When followUpContext is present, include the original query and Q&A chain + // so the model can extract places/topic from the full conversation. + let contents: string; + if (followUpContext) { + const chain = followUpContext.followUps + .map((f) => `Q: "${f.question}" → A: "${f.answer}"`) + .join('\n'); + contents = `Original query: "${followUpContext.originalQuery}"\nFollow-up clarifications:\n${chain}\n\nCurrent query: "${query}"`; + } else { + contents = `Query: "${query}"`; + } + const response = await genAI.models.generateContent({ model: config.models.parseQuery, - contents: `Query: "${query}"`, + contents, config: { systemInstruction: systemPrompt }, }); @@ -43,13 +56,13 @@ export const parseQuery = async ( places: Array.isArray(parsed.places) ? parsed.places : [query], topic: parsed.topic || query, titles: parsed.titles || {}, - isFollowUp: !!parsed.isFollowUp, + isFollowUp: !!parsed.isFollowUp || !!followUpContext, dateRange: parsed.dateRange || undefined, } : { places: [query], topic: query, titles: {}, - isFollowUp: false, + isFollowUp: !!followUpContext, }; }; diff --git a/dataweaver/apps/web/src/server/types.ts b/dataweaver/apps/web/src/server/types.ts index 3b0b6539..0592febf 100644 --- a/dataweaver/apps/web/src/server/types.ts +++ b/dataweaver/apps/web/src/server/types.ts @@ -19,6 +19,7 @@ export interface HistoryNode { timestamp: number; status: 'pending' | 'complete' | 'error'; followUp?: FollowUp; + followUpContext?: FollowUpContext; } export type CardType = 'loading' | 'table' | 'notes' | 'chart'; @@ -57,6 +58,16 @@ export interface FollowUp { options: string[]; } +export interface FollowUpEntry { + question: string; + answer: string; +} + +export interface FollowUpContext { + originalQuery: string; + followUps: FollowUpEntry[]; +} + export interface QueryResult { id: string; title: string; @@ -115,6 +126,7 @@ export interface QueryStreamRequest { atlasContext: string; ancestorChain: { query: string; topic: string; places: string[] }[]; selectedEntityDcids: string[]; + followUpContext?: FollowUpContext; } // --- Status messages --- diff --git a/dataweaver/apps/web/src/store/store.ts b/dataweaver/apps/web/src/store/store.ts index ef320863..dce6044d 100644 --- a/dataweaver/apps/web/src/store/store.ts +++ b/dataweaver/apps/web/src/store/store.ts @@ -4,6 +4,7 @@ import { devtools, subscribeWithSelector } from 'zustand/middleware'; import type { CardEntry, CardType, + FollowUpContext, HistoryNode, ParsedQuery, QueryResult, @@ -28,6 +29,7 @@ export interface AtlasStore { query: string, parsedQuery: ParsedQuery | null, parentNodeId: string | null, + followUpContext?: FollowUpContext, ) => string; nodeSetParsedQuery: (nodeId: string, parsedQuery: ParsedQuery) => void; nodeAddResult: ( @@ -71,7 +73,7 @@ export const useAtlasStore = create()( isProcessing: false, currentStatus: '', - queryStart: (query, parsedQuery, parentNodeId) => { + queryStart: (query, parsedQuery, parentNodeId, followUpContext) => { const id = nanoid(); const node: HistoryNode = { id, @@ -82,6 +84,7 @@ export const useAtlasStore = create()( cardIds: [], timestamp: Date.now(), status: 'pending', + followUpContext, }; set( (state) => ({ From ffcc1c40356283a605b6741547f2df4cc7a62690 Mon Sep 17 00:00:00 2001 From: Scott Agrimson Date: Tue, 30 Jun 2026 17:26:46 -0700 Subject: [PATCH 03/18] add skill updates --- .../apps/web/src/server/config/skills/data_discovery.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dataweaver/apps/web/src/server/config/skills/data_discovery.md b/dataweaver/apps/web/src/server/config/skills/data_discovery.md index 5f42e4d4..a8e9a664 100644 --- a/dataweaver/apps/web/src/server/config/skills/data_discovery.md +++ b/dataweaver/apps/web/src/server/config/skills/data_discovery.md @@ -53,3 +53,8 @@ RULES: - **`summary`**: Write a conversational paragraph using actual observation values you retrieved (e.g. population figures, GDP, life expectancy). End the summary naturally — do NOT include the question inside the summary. Use plain text, do not include markdown formatting. - **`question`**: A short, inviting follow-up question (e.g. "What specifically would you like to know?"). Use plain text, do not include markdown formatting. - **`options`**: Exactly 3 to 5 concise labels. Each should represent a distinct analytical direction relevant to the place/topic. Phrase them from the user's perspective (e.g. "Economic impact", "Health outcomes for its people", not "GDP variables"). +6. **Disable Topics in Search**: When calling the `search_indicators` tool, you MUST set the `include_topics` parameter to `false` to ensure you only retrieve variables and not broad topics/categories. +7. **Global / World DCID**: If the query is about the entire world or global trends, the official Data Commons DCID is `Earth`. Do NOT use `World`, `global`, or `country/WLD` as the DCID. +8. **Always Pass Place to Search**: When calling the `search_indicators` tool, you MUST always pass the TARGET PLACE in the `places` array parameter. This is critical for the system to resolve the correct location DCID and verify data availability. +9. **Verify Data Availability**: You MUST only select statistical variables that have the target place's resolved DCID in their `places_with_data` list returned by the `search_indicators` tool. Do NOT assume or guess that a standard variable will work for a continent or region if it is not explicitly listed in the tool's response as having data for that place. +10. **Do NOT Guess or Hallucinate DCIDs**: Data Commons DCIDs are highly specific, case-sensitive, and do not follow a simple pattern (e.g. `Count_Person_Employed` instead of `employed_persons`, or `Count_Person_15To64Years_InLaborForce_AsFractionOf_Count_Person_15To64Years` instead of `labor_force_participation_rate`). You MUST copy the `dcid` EXACTLY as returned in the MCP tool's response. Never invent, guess, or modify a DCID. \ No newline at end of file From e985e25eefcf2fd5fec39871f9fc96bdec9f57cf Mon Sep 17 00:00:00 2001 From: Scott Agrimson Date: Wed, 1 Jul 2026 15:57:16 -0700 Subject: [PATCH 04/18] try to get better options in follow up, add an array of placeholder sample questions to return if the api returns no options, edit discovery skill to only show follow up if there is no data --- .../components/scopes/page_home/follow_up.tsx | 16 +++++++++----- .../apps/web/src/configs/example_prompts.ts | 22 +++++++++++++++++++ .../apps/web/src/functions/shuffle_array.ts | 8 +++++++ .../server/config/skills/data_discovery.md | 20 ++++++++--------- 4 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 dataweaver/apps/web/src/configs/example_prompts.ts create mode 100644 dataweaver/apps/web/src/functions/shuffle_array.ts diff --git a/dataweaver/apps/web/src/components/scopes/page_home/follow_up.tsx b/dataweaver/apps/web/src/components/scopes/page_home/follow_up.tsx index 2b121c2d..3945e364 100644 --- a/dataweaver/apps/web/src/components/scopes/page_home/follow_up.tsx +++ b/dataweaver/apps/web/src/components/scopes/page_home/follow_up.tsx @@ -2,8 +2,11 @@ import { EASE_LINEAR } from '@package/tokens/ts'; import { m } from 'motion/react'; +import { useMemo } from 'react'; import { Button } from '~/components/elements/button'; import { IconClose } from '~/components/primitives/icons/close'; +import { EXAMPLE_PROMPTS } from '~/configs/example_prompts'; +import { shuffleArray } from '~/functions/shuffle_array'; import type { FollowUp as FollowUpData } from '~/server/types'; import s from './follow_up.module.scss'; @@ -20,6 +23,12 @@ export const FollowUp = ({ onSelect, onClose, }: FollowUpProps) => { + const options = useMemo(() => { + return followUp?.options?.length + ? followUp.options + : shuffleArray(EXAMPLE_PROMPTS).slice(0, 4); + }, [followUp?.options]); + return (

{prompt}

-
{followUp.summary}
-
{followUp.question}
- - {followUp?.options?.length > 0 && ( + {options.length > 0 && (
    - {followUp.options.map((option) => ( + {options.map((option) => (
  • + {relatedQueries.map((query) => ( + + ))} )} diff --git a/dataweaver/apps/web/src/components/elements/card/footer.module.scss b/dataweaver/apps/web/src/components/elements/card/footer.module.scss index f49860e9..d06d0c3c 100644 --- a/dataweaver/apps/web/src/components/elements/card/footer.module.scss +++ b/dataweaver/apps/web/src/components/elements/card/footer.module.scss @@ -1,4 +1,8 @@ .container { + display: flex; flex-shrink: 0; + flex-flow: column; + gap: 8px; + align-items: flex-start; padding: 0 28px 18px; } diff --git a/dataweaver/apps/web/src/components/elements/card/text.tsx b/dataweaver/apps/web/src/components/elements/card/text.tsx index ee0812ca..ad1bc182 100644 --- a/dataweaver/apps/web/src/components/elements/card/text.tsx +++ b/dataweaver/apps/web/src/components/elements/card/text.tsx @@ -41,7 +41,7 @@ export interface CardTextProps extends CardState { /** HTML string rendered via `html-react-parser` (sanitized/filtered). */ body?: string; - relatedQuery?: string; + relatedQueries?: string[]; } export const CardText = ({ @@ -50,7 +50,7 @@ export const CardText = ({ selection, title, body, - relatedQuery, + relatedQueries, }: CardTextProps) => { const editor = useEditor(); @@ -106,18 +106,21 @@ export const CardText = ({ )} - {relatedQuery && !isLoading && ( + {relatedQueries && relatedQueries.length > 0 && !isLoading && ( - + {relatedQueries.map((query) => ( + + ))} )} diff --git a/dataweaver/apps/web/src/components/scopes/atlas/helpers.ts b/dataweaver/apps/web/src/components/scopes/atlas/helpers.ts index 4d9605af..f9e8fbf4 100644 --- a/dataweaver/apps/web/src/components/scopes/atlas/helpers.ts +++ b/dataweaver/apps/web/src/components/scopes/atlas/helpers.ts @@ -5,7 +5,7 @@ import type { CardTextProps } from '~/components/elements/card/text'; import { CARD_VARIANT_MAX } from './config'; interface BaseContent extends Partial> { - relatedQuery?: string; + relatedQueries?: string[]; } interface TextContent @@ -67,7 +67,7 @@ export const contentToShape = ( // worst-case space up front means initial placement never overlaps neighbor ...CARD_VARIANT_MAX[content.variant], isLoading: content.isLoading ?? false, - relatedQuery: content.relatedQuery, + relatedQueries: content.relatedQueries, }; if (content.variant === 'chart') { diff --git a/dataweaver/apps/web/src/components/scopes/atlas/shapes/card.tsx b/dataweaver/apps/web/src/components/scopes/atlas/shapes/card.tsx index 19994104..1854ff27 100644 --- a/dataweaver/apps/web/src/components/scopes/atlas/shapes/card.tsx +++ b/dataweaver/apps/web/src/components/scopes/atlas/shapes/card.tsx @@ -54,7 +54,7 @@ export class ShapeCardUtil extends ShapeUtil { }), ).optional(), isLoading: T.boolean.optional(), - relatedQuery: T.string.optional(), + relatedQueries: T.arrayOf(T.string).optional(), }; override getDefaultProps = (): ShapeCardProps => { @@ -79,7 +79,7 @@ export class ShapeCardUtil extends ShapeUtil { // Each card variant owns its own actions, content and footer; the shape just // hands it the state and content it needs to render itself. #renderCard = (shape: ShapeCard) => { - const { variant, title, description, body, data, relatedQuery } = + const { variant, title, description, body, data, relatedQueries } = shape.props; const isLoading = shape.props.isLoading ?? false; @@ -93,7 +93,7 @@ export class ShapeCardUtil extends ShapeUtil { selection={selection} title={title} body={body} - relatedQuery={relatedQuery} + relatedQueries={relatedQueries} /> ); } @@ -107,7 +107,7 @@ export class ShapeCardUtil extends ShapeUtil { title={title} description={description} data={data} - relatedQuery={relatedQuery} + relatedQueries={relatedQueries} /> ); } diff --git a/dataweaver/apps/web/src/components/scopes/atlas/sync_store.ts b/dataweaver/apps/web/src/components/scopes/atlas/sync_store.ts index 95413967..00647fbe 100644 --- a/dataweaver/apps/web/src/components/scopes/atlas/sync_store.ts +++ b/dataweaver/apps/web/src/components/scopes/atlas/sync_store.ts @@ -30,7 +30,7 @@ export const deriveNotesContent = (result: QueryResult): AtlasContent => ({ title: `${result.title} • Notes`, body: result.notesHtml ?? '', isLoading: false, - relatedQuery: result.relatedQueries?.[0], + relatedQueries: result.relatedQueries, }); /** Derive AtlasContent for a chart card from a QueryResult (first variable's facets). */ diff --git a/dataweaver/apps/web/src/configs/mock_query.ts b/dataweaver/apps/web/src/configs/mock_query.ts index cd8f464c..4bf0f1b5 100644 --- a/dataweaver/apps/web/src/configs/mock_query.ts +++ b/dataweaver/apps/web/src/configs/mock_query.ts @@ -20,7 +20,10 @@ const MOCK_RESPONSES: MockResponse[] = [ variant: 'text', title: 'Key insights when evaluating greenhouse gas emissions', body: 'Emissions per capita remain low relative to other regions, energy access is the dominant driver, and land-use change accounts for a large share of the total.', - relatedQuery: 'What are the key drivers of these trends?', + relatedQueries: [ + 'What are the key drivers of these trends?', + 'How does energy access compare across Africa?', + ], }, }, { @@ -48,7 +51,10 @@ const MOCK_RESPONSES: MockResponse[] = [ variant: 'text', title: 'Why European emissions have been falling', body: 'A sustained shift away from coal, strong efficiency standards, and rapid renewable deployment have driven emissions down even as the economy has continued to grow.', - relatedQuery: 'Which policies drove the steepest cuts?', + relatedQueries: [ + 'Which policies drove the steepest cuts?', + 'How has renewable energy grown in Europe?', + ], }, }, { @@ -68,7 +74,7 @@ const MOCK_RESPONSES: MockResponse[] = [ { date: '2018', value: 5614 }, { date: '2021', value: 5265 }, ], - relatedQuery: 'How does this compare to other regions?', + relatedQueries: ['How does this compare to other regions?'], }, }, { @@ -77,7 +83,10 @@ const MOCK_RESPONSES: MockResponse[] = [ variant: 'text', title: 'What is driving emissions growth in Asia', body: 'Rapid industrialisation, rising energy demand, and continued reliance on coal have made the region the largest contributor to global emissions growth over the past two decades.', - relatedQuery: 'How might this trajectory change?', + relatedQueries: [ + 'How might this trajectory change?', + 'What is coal dependency in Asia?', + ], }, }, { @@ -97,7 +106,7 @@ const MOCK_RESPONSES: MockResponse[] = [ { date: '2018', value: 20366 }, { date: '2021', value: 21521 }, ], - relatedQuery: 'What would a lower-emissions pathway require?', + relatedQueries: ['What would a lower-emissions pathway require?'], }, }, ]; diff --git a/dataweaver/apps/web/src/server/config/skills/data_discovery.md b/dataweaver/apps/web/src/server/config/skills/data_discovery.md index 96324370..103c027b 100644 --- a/dataweaver/apps/web/src/server/config/skills/data_discovery.md +++ b/dataweaver/apps/web/src/server/config/skills/data_discovery.md @@ -29,6 +29,9 @@ JSON SCHEMA: "text": "A brief summary describing the trends, milestones, maximum/minimum levels, and direction changes for the data." } ], + "relatedQueries": [ + "1 to 3 short natural-language questions (~7–8 words each) suggesting adjacent data topics the user might explore next for this place. Must be grounded in variables or topic categories that appeared in search_indicators results with confirmed data availability for the target place." + ], "followUp": { "summary": "A conversational paragraph summarizing what you DO know about the place or topic so far, if anything (key stats like population, GDP, life expectancy, etc.), using the observation data you retrieved. Should read naturally, not as a list.", "question": "A follow-up question inviting the user to narrow their focus (e.g. 'What would you like to explore?') Plus a call to action (e.g. 'Pick a topic below or type your own question.').", @@ -57,4 +60,9 @@ RULES: 7. **Global / World DCID**: If the query is about the entire world or global trends, the official Data Commons DCID is `Earth`. Do NOT use `World`, `global`, or `country/WLD` as the DCID. 8. **Always Pass Place to Search**: When calling the `search_indicators` tool, you MUST always pass the TARGET PLACE in the `places` array parameter. This is critical for the system to resolve the correct location DCID and verify data availability. 9. **Verify Data Availability**: You MUST only select statistical variables that have the target place's resolved DCID in their `places_with_data` list returned by the `search_indicators` tool. Do NOT assume or guess that a standard variable will work for a continent or region if it is not explicitly listed in the tool's response as having data for that place. -10. **Do NOT Guess or Hallucinate DCIDs**: Data Commons DCIDs are highly specific, case-sensitive, and do not follow a simple pattern (e.g. `Count_Person_Employed` instead of `employed_persons`, or `Count_Person_15To64Years_InLaborForce_AsFractionOf_Count_Person_15To64Years` instead of `labor_force_participation_rate`). You MUST copy the `dcid` EXACTLY as returned in the MCP tool's response. Never invent, guess, or modify a DCID. \ No newline at end of file +10. **Do NOT Guess or Hallucinate DCIDs**: Data Commons DCIDs are highly specific, case-sensitive, and do not follow a simple pattern (e.g. `Count_Person_Employed` instead of `employed_persons`, or `Count_Person_15To64Years_InLaborForce_AsFractionOf_Count_Person_15To64Years` instead of `labor_force_participation_rate`). You MUST copy the `dcid` EXACTLY as returned in the MCP tool's response. Never invent, guess, or modify a DCID. +11. **Related Queries**: When you successfully return variables and data (i.e. `followUp` is NOT used), you MUST include `relatedQueries` — an array of 1 to 3 short questions suggesting what the user could explore next. + - Each question MUST be approximately 7–8 words long (e.g. "How has poverty changed in Japan?", "What are literacy rates in Africa?", "How does renewable energy compare here?"). + - Queries MUST be derived from variables or topic categories you encountered during `search_indicators` calls where the target place's DCID appeared in `places_with_data` — but that you did NOT select as primary results. Do NOT suggest queries about the same variables you are already returning. + - If you did not encounter any adjacent verified topics during your search, include only 1 query based on a broader related theme you are confident has data (e.g. a parent topic category that appeared in results). + - Omit `relatedQueries` entirely when `followUp` is present (no data found). \ No newline at end of file From bb3b153b97ad2acbe83e89382246fe7ab42ab7ee Mon Sep 17 00:00:00 2001 From: Scott Agrimson Date: Thu, 2 Jul 2026 16:37:24 -0700 Subject: [PATCH 10/18] use example prompts array in intro card --- dataweaver/apps/web/src/app/(atlas)/page.tsx | 9 ++++++++- .../web/src/components/scopes/page_home/intro.tsx | 12 +++--------- .../src/components/scopes/page_home/page_home.tsx | 8 +++++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/dataweaver/apps/web/src/app/(atlas)/page.tsx b/dataweaver/apps/web/src/app/(atlas)/page.tsx index 7eebd378..c03b56a4 100644 --- a/dataweaver/apps/web/src/app/(atlas)/page.tsx +++ b/dataweaver/apps/web/src/app/(atlas)/page.tsx @@ -1,3 +1,10 @@ import { PageHome } from '~/components/scopes/page_home/page_home'; +import { EXAMPLE_PROMPTS } from '~/configs/example_prompts'; +import { shuffleArray } from '~/functions/shuffle_array'; -export default PageHome; +const Page = () => { + const prompts = shuffleArray(EXAMPLE_PROMPTS).slice(0, 4); + return ; +}; + +export default Page; diff --git a/dataweaver/apps/web/src/components/scopes/page_home/intro.tsx b/dataweaver/apps/web/src/components/scopes/page_home/intro.tsx index 7b1bcb2e..3d3ffb9d 100644 --- a/dataweaver/apps/web/src/components/scopes/page_home/intro.tsx +++ b/dataweaver/apps/web/src/components/scopes/page_home/intro.tsx @@ -5,19 +5,13 @@ import { IconClose } from '~/components/primitives/icons/close'; import { IconInfo } from '~/components/primitives/icons/info'; import s from './intro.module.scss'; -export const EXAMPLE_PROMPTS = [ - 'How access to electricity has changed across countries in Africa?', - 'Which countries have the highest access to mobile phones?', - 'Compare GDP and population growth globally over the last 50 years.', - 'What fraction of the world is forest?', -] as const; - interface IntroProps { onSelect: (example: string, index: number) => void; onClose: () => void; + prompts: string[]; } -export const Intro = ({ onSelect, onClose }: IntroProps) => { +export const Intro = ({ onSelect, onClose, prompts }: IntroProps) => { return ( {

      - {EXAMPLE_PROMPTS.map((example, index) => ( + {prompts.map((example, index) => (