}) {
+ const [data, error, pending] = useStreamableValue(result);
+
+ if (pending && !data) {
+ return (
+
+ );
+ }
+
+ return ;
+}
+
+function GraphCard({ data, pending }: { data: any, pending?: boolean }) {
+ const chartData: DataAnalysisResult | undefined = React.useMemo(() => {
+ if (!data) return undefined;
+ if (typeof data === 'string') {
+ try {
+ return JSON.parse(data);
+ } catch (e) {
+ console.error('Error parsing graph data:', e);
+ return undefined;
+ }
+ }
+ return data as DataAnalysisResult;
+ }, [data]);
+
+ if (!chartData) return null;
+
+ const { title, description, chartType, data: plotData, config } = chartData;
+
+ const renderChart = () => {
+ if (!plotData || !config) return Missing chart data or configuration
;
+
+ const themeColors = {
+ text: 'hsl(var(--foreground))',
+ grid: 'hsl(var(--border))',
+ tooltip: {
+ bg: 'hsl(var(--card))',
+ text: 'hsl(var(--card-foreground))',
+ border: 'hsl(var(--border))'
+ }
+ }
+
+ const commonAxisProps = {
+ stroke: themeColors.text,
+ fontSize: 12,
+ tickLine: false,
+ axisLine: false,
+ }
+
+ switch (chartType) {
+ case 'bar':
+ return (
+
+
+
+
+
+
+
+ {config.series?.map((s, i) => (
+
+ ))}
+
+
+ );
+ case 'line':
+ return (
+
+
+
+
+
+
+
+ {config.series?.map((s, i) => (
+
+ ))}
+
+
+ );
+ case 'area':
+ return (
+
+
+
+
+
+
+
+ {config.series?.map((s, i) => (
+
+ ))}
+
+
+ );
+ case 'pie':
+ return (
+
+
+
+ {plotData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+ );
+ case 'scatter':
+ return (
+
+
+
+
+
+
+
+ {config.series?.map((s, i) => (
+
+ ))}
+
+
+ );
+ default:
+ return (
+
+ Unsupported chart type: {chartType || 'None'}
+
+ );
+ }
+ };
+
+ return (
+
+
+ Graph: {title || 'Untitled'}
+
+
+
+ {title || 'Data Analysis'}
+ {description && {description}}
+
+
+
+ {renderChart()}
+
+
+
+
+ );
+}
diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx
index eecd7f54..ef876721 100644
--- a/components/map/mapbox-map.tsx
+++ b/components/map/mapbox-map.tsx
@@ -467,7 +467,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
geolocationWatchIdRef.current = null
}
}
- }, [setMap, setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation])
+ }, [setMap, setIsMapLoaded, captureMapCenter, handleUserInteraction, stopRotation, position?.latitude, position?.longitude, mapData.cameraState])
// Handle map mode changes
useEffect(() => {
diff --git a/lib/agents/researcher.tsx b/lib/agents/researcher.tsx
index 72a5d737..4cf94a59 100644
--- a/lib/agents/researcher.tsx
+++ b/lib/agents/researcher.tsx
@@ -42,7 +42,19 @@ Current date and time: ${date}.
ONLY when the user explicitly provides one or more URLs and asks you to read, summarize, or extract content from them.
- **Never use** this tool proactively.
-#### **3. Location, Geography, Navigation, and Mapping Queries**
+#### **3. Data Analysis and Visualization**
+- **Tool**: \`dataAnalysis\`
+- **When to use**:
+ Any query asking for a chart, graph, or visual representation of data. Use it when you have structured data (e.g., from web search or uploaded CSV/JSON files) that would be clearer in a visual format.
+- **Capabilities**: Can generate bar, line, pie, area, and scatter charts. It can also include geospatial points if the data has location information.
+
+**Examples that trigger \`dataAnalysis\`:**
+- "Create a bar chart showing the population of the top 5 largest cities"
+- "Plot a line graph of NVIDIA's stock price over the last 6 months"
+- "Show me a pie chart of my expenses from this uploaded CSV"
+- "Visualize the relationship between height and weight from this data as a scatter plot"
+
+#### **4. Location, Geography, Navigation, and Mapping Queries**
- **Tool**: \`geospatialQueryTool\` → **MUST be used (no exceptions)** for:
• Finding places, businesses, "near me", distances, directions
• Travel times, routes, traffic, map generation
@@ -63,9 +75,10 @@ Current date and time: ${date}.
#### **Summary of Decision Flow**
1. User gave explicit URLs? → \`retrieve\`
-2. Location/distance/direction/maps? → \`geospatialQueryTool\` (mandatory)
-3. Everything else needing external data? → \`search\`
-4. Otherwise → answer from knowledge
+2. Visualization/Chart/Graph requested? → \`dataAnalysis\`
+3. Location/distance/direction/maps? → \`geospatialQueryTool\` (mandatory)
+4. Everything else needing external data? → \`search\`
+5. Otherwise → answer from knowledge
These rules override all previous instructions.
diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx
index 1acd0e01..88dd38e8 100644
--- a/lib/agents/resolution-search.tsx
+++ b/lib/agents/resolution-search.tsx
@@ -1,4 +1,4 @@
-import { CoreMessage, streamObject } from 'ai'
+import { CoreMessage, generateObject } from 'ai'
import { getModel } from '@/lib/utils'
import { z } from 'zod'
@@ -23,14 +23,7 @@ const resolutionSearchSchema = z.object({
}).describe('A GeoJSON object containing points of interest and classified land features to be overlaid on the map.'),
})
-export interface DrawnFeature {
- id: string;
- type: 'Polygon' | 'LineString';
- measurement: string;
- geometry: any;
-}
-
-export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC', drawnFeatures?: DrawnFeature[]) {
+export async function resolutionSearch(messages: CoreMessage[], timezone: string = 'UTC') {
const localTime = new Date().toLocaleString('en-US', {
timeZone: timezone,
hour: '2-digit',
@@ -45,11 +38,6 @@ export async function resolutionSearch(messages: CoreMessage[], timezone: string
const systemPrompt = `
As a geospatial analyst, your task is to analyze the provided satellite image of a geographic location.
The current local time at this location is ${localTime}.
-
-${drawnFeatures && drawnFeatures.length > 0 ? `The user has drawn the following features on the map for your reference:
-${drawnFeatures.map(f => `- ${f.type} with measurement ${f.measurement}`).join('\n')}
-Use these user-drawn areas/lines as primary areas of interest for your analysis.` : ''}
-
Your analysis should be comprehensive and include the following components:
1. **Land Feature Classification:** Identify and describe the different types of land cover visible in the image (e.g., urban areas, forests, water bodies, agricultural fields).
@@ -69,11 +57,14 @@ Analyze the user's prompt and the image to provide a holistic understanding of t
message.content.some(part => part.type === 'image')
)
- // Use streamObject to get partial results.
- return streamObject({
+ // Use generateObject to get the full object at once.
+ const { object } = await generateObject({
model: await getModel(hasImage),
system: systemPrompt,
messages: filteredMessages,
schema: resolutionSearchSchema,
})
+
+ // Return the complete, validated object.
+ return object
}
\ No newline at end of file
diff --git a/lib/agents/tools/data-analysis.tsx b/lib/agents/tools/data-analysis.tsx
new file mode 100644
index 00000000..698b243c
--- /dev/null
+++ b/lib/agents/tools/data-analysis.tsx
@@ -0,0 +1,19 @@
+import { createStreamableValue } from 'ai/rsc'
+import { dataAnalysisSchema } from '@/lib/schema/data-analysis'
+import { ToolProps } from '.'
+import { DataAnalysisResult } from '@/lib/types'
+import { GraphSection } from '@/components/graph-section'
+
+export const dataAnalysisTool = ({ uiStream }: ToolProps) => ({
+ description: 'Analyze data and generate a structured representation for visualization in a graph or chart. Use this tool when the user asks for a chart, graph, or data visualization, or when you have structured data (like from a CSV or search results) that would be better understood visually.',
+ parameters: dataAnalysisSchema,
+ execute: async (result: DataAnalysisResult) => {
+ const streamResults = createStreamableValue()
+
+ uiStream.append()
+
+ streamResults.done(result)
+
+ return result
+ }
+})
diff --git a/lib/agents/tools/index.tsx b/lib/agents/tools/index.tsx
index 4c22b887..58ac23df 100644
--- a/lib/agents/tools/index.tsx
+++ b/lib/agents/tools/index.tsx
@@ -3,6 +3,7 @@ import { retrieveTool } from './retrieve'
import { searchTool } from './search'
import { videoSearchTool } from './video-search'
import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import
+import { dataAnalysisTool } from './data-analysis'
import { MapProvider } from '@/lib/store/settings'
@@ -25,6 +26,10 @@ export const getTools = ({ uiStream, fullResponse, mapProvider }: ToolProps) =>
geospatialQueryTool: geospatialTool({
uiStream,
mapProvider
+ }),
+ dataAnalysis: dataAnalysisTool({
+ uiStream,
+ fullResponse
})
}
diff --git a/lib/schema/data-analysis.tsx b/lib/schema/data-analysis.tsx
new file mode 100644
index 00000000..aa753bcd
--- /dev/null
+++ b/lib/schema/data-analysis.tsx
@@ -0,0 +1,25 @@
+import { DeepPartial } from 'ai'
+import { z } from 'zod'
+
+export const dataAnalysisSchema = z.object({
+ title: z.string().describe('The title of the chart'),
+ description: z.string().optional().describe('A brief description of the chart'),
+ chartType: z.enum(['bar', 'line', 'pie', 'area', 'scatter']).describe('The type of chart to render'),
+ data: z.array(z.record(z.any())).describe('The data points for the chart'),
+ config: z.object({
+ xAxisKey: z.string().describe('The key in the data object to use for the X axis'),
+ yAxisKey: z.string().optional().describe('The key in the data object to use for the Y axis (for scatter charts)'),
+ series: z.array(z.object({
+ key: z.string().describe('The key in the data object for this series'),
+ name: z.string().describe('The display name for this series'),
+ color: z.string().optional().describe('Optional hex color for this series')
+ })).describe('The series to be plotted')
+ }).describe('Configuration for the chart layout'),
+ geospatial: z.array(z.object({
+ latitude: z.number(),
+ longitude: z.number(),
+ label: z.string().optional()
+ })).optional().describe('Optional geospatial data points to be displayed on a map')
+})
+
+export type PartialDataAnalysis = DeepPartial
diff --git a/lib/types/index.ts b/lib/types/index.ts
index c4ea616c..8bd92cc9 100644
--- a/lib/types/index.ts
+++ b/lib/types/index.ts
@@ -25,6 +25,27 @@ export type SearchResultItem = {
content: string
}
+export type DataAnalysisResult = {
+ title: string;
+ description?: string;
+ chartType: 'bar' | 'line' | 'pie' | 'area' | 'scatter';
+ data: any[];
+ config: {
+ xAxisKey: string;
+ yAxisKey?: string;
+ series: {
+ key: string;
+ name: string;
+ color?: string;
+ }[];
+ };
+ geospatial?: {
+ latitude: number;
+ longitude: number;
+ label?: string;
+ }[];
+};
+
export type ExaSearchResultItem = {
score: number
title: string
diff --git a/package.json b/package.json
index cdf8a7e3..b0ee3f6d 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"react-markdown": "^9.1.0",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^10.0.6",
+ "recharts": "^3.7.0",
"rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
diff --git a/verification/fix_verification.png b/verification/fix_verification.png
new file mode 100644
index 00000000..67df6fab
Binary files /dev/null and b/verification/fix_verification.png differ