Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
573 changes: 87 additions & 486 deletions app/actions.tsx

Large diffs are not rendered by default.

75 changes: 74 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="text/plain,image/png,image/jpeg,image/webp"
accept="text/plain,image/png,image/jpeg,image/webp,text/csv,application/json"
/>
{!isMobile && (
<Button
Expand Down
201 changes: 201 additions & 0 deletions components/graph-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
'use client'

import React from 'react'
import {
BarChart,
Bar,
LineChart,
Line,
PieChart,
Pie,
AreaChart,
Area,
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
Cell
} from 'recharts'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Section } from './section'
import { ToolBadge } from './tool-badge'
import { DataAnalysisResult } from '@/lib/types'
import { StreamableValue, useStreamableValue } from 'ai/rsc'

const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Hardcoded colors won't adapt to the site's theme.

The COLORS array uses fixed hex values. Per project requirements, these should be derived from CSS variables (--primary, --secondary, --accent, --muted) to match the active theme (light/dark/earth). Recharts requires hex colors, so the CSS variable values need to be read at runtime and converted.

♻️ Suggested approach
// Example utility to read a CSS variable and return a hex color string
function getCSSColor(variable: string, fallback: string): string {
  if (typeof window === 'undefined') return fallback;
  const value = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
  // Convert HSL to hex if needed, or return the raw value
  return value || fallback;
}

// Then in GraphCard (or via a hook), build COLORS dynamically:
const themeColors = useMemo(() => [
  getCSSColor('--primary', '#0088FE'),
  getCSSColor('--secondary', '#00C49F'),
  getCSSColor('--accent', '#FFBB28'),
  getCSSColor('--muted', '#FF8042'),
  // ...additional fallbacks
], [/* re-run on theme change */]);

Based on learnings: "The GraphSection component in components/graph-section.tsx should use theme-aware colors that match the site's current theme (light, dark, or earth) by reading CSS variables like --primary, --secondary, --accent, and --muted, and converting them to hex colors for Recharts compatibility."

🤖 Prompt for AI Agents
In `@components/graph-section.tsx` at line 29, Replace the hardcoded COLORS array
in GraphSection with theme-aware colors by reading CSS variables at runtime:
implement a utility like getCSSColor(variable, fallback) that uses
getComputedStyle(document.documentElement).getPropertyValue and normalizes
returned values to hex (handling hex, rgb(a), or hsl) and then build COLORS via
useMemo inside the GraphSection component (or a small hook) to return
[getCSSColor('--primary', '#0088FE'), getCSSColor('--secondary', '#00C49F'),
getCSSColor('--accent', '#FFBB28'), getCSSColor('--muted', '#FF8042'), ...];
ensure server-side rendering safety by returning fallbacks when window is
undefined and re-compute on theme changes.


interface GraphSectionProps {
result: DataAnalysisResult | string | StreamableValue<DataAnalysisResult>
}

export function GraphSection({ result }: GraphSectionProps) {
if (!result) return null;

// Check if result is a static DataAnalysisResult object
// A StreamableValue is an opaque object and shouldn't have these properties
const isStatic = typeof result === 'object' && result !== null &&
('chartType' in (result as any) || 'title' in (result as any) || 'data' in (result as any));
const isString = typeof result === 'string';

if (isStatic || isString) {
return <GraphCard data={result as any} />;
}

// Handle case where it might be a streamable value or something else
// We use a safe wrapper to avoid crashing if useStreamableValue throws
return <StreamedGraphSection result={result as any} />;
}
Comment on lines +35 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duck-typing with generic property names is fragile for distinguishing StreamableValue from DataAnalysisResult.

Checking for 'data' in result is risky since 'data' is a very common property name and internal implementations of StreamableValue could expose it. Consider checking for a more distinctive property (e.g., only chartType) or using a type-guard that positively identifies the DataAnalysisResult shape.

♻️ Suggested refinement
- const isStatic = typeof result === 'object' && result !== null &&
-   ('chartType' in (result as any) || 'title' in (result as any) || 'data' in (result as any));
+ const isStatic = typeof result === 'object' && result !== null &&
+   'chartType' in (result as any);

Using chartType alone is sufficient — it's specific to DataAnalysisResult and won't appear on a StreamableValue.

🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 35 - 51, The current GraphSection
uses fragile duck-typing by checking for 'data' and other common keys to detect
a DataAnalysisResult; update the type-guard in GraphSection to positively
identify DataAnalysisResult by checking only for the distinctive chartType
property (e.g., change the isStatic check to test `'chartType' in result` and
remove the 'data'/'title' checks), and ensure downstream branches still pass the
value to GraphCard or StreamedGraphSection unchanged (referencing GraphSection,
GraphCard, StreamedGraphSection, and the result param).


function StreamedGraphSection({ result }: { result: StreamableValue<any> }) {
const [data, error, pending] = useStreamableValue(result);

if (pending && !data) {
return (
<Section className="py-2">
<div className="animate-pulse flex space-y-4 flex-col">
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-64 bg-muted rounded"></div>
</div>
</Section>
);
}

return <GraphCard data={data} />;
}
Comment on lines +53 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stream error is silently discarded.

error is destructured from useStreamableValue but never checked. If the stream errors, the user sees nothing (GraphCard renders null for falsy data) with no error indication.

🐛 Proposed fix
 function StreamedGraphSection({ result }: { result: StreamableValue<any> }) {
   const [data, error, pending] = useStreamableValue(result);
 
+  if (error) {
+    return (
+      <Section className="py-2">
+        <div className="text-destructive text-sm">Failed to load chart data.</div>
+      </Section>
+    );
+  }
+
   if (pending && !data) {
     return (
       <Section className="py-2">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function StreamedGraphSection({ result }: { result: StreamableValue<any> }) {
const [data, error, pending] = useStreamableValue(result);
if (pending && !data) {
return (
<Section className="py-2">
<div className="animate-pulse flex space-y-4 flex-col">
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-64 bg-muted rounded"></div>
</div>
</Section>
);
}
return <GraphCard data={data} />;
}
function StreamedGraphSection({ result }: { result: StreamableValue<any> }) {
const [data, error, pending] = useStreamableValue(result);
if (error) {
return (
<Section className="py-2">
<div className="text-destructive text-sm">Failed to load chart data.</div>
</Section>
);
}
if (pending && !data) {
return (
<Section className="py-2">
<div className="animate-pulse flex space-y-4 flex-col">
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-64 bg-muted rounded"></div>
</div>
</Section>
);
}
return <GraphCard data={data} />;
}
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 53 - 68, StreamedGraphSection
currently ignores the error value returned by useStreamableValue; update
StreamedGraphSection to check the destructured error and render an error state
(e.g., an error message/card inside the same Section or return a dedicated
ErrorCard) when error is truthy instead of falling through to GraphCard with
null data; reference the useStreamableValue call and GraphCard render so the
component returns a clear error UI when error is present and only renders
GraphCard when data is valid and no error exists.

Comment on lines +35 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphSection uses multiple any-based escape hatches (result as any, StreamableValue<any>, data: any), and uses an in-operator heuristic to distinguish streamable vs static values.

This makes it easy for malformed data to slip through and cause runtime errors inside Recharts (which can be hard to debug). Since you already own DataAnalysisResult, you can tighten this significantly:

  • avoid any in prop types
  • use a small type guard for DataAnalysisResult
  • pass StreamableValue<DataAnalysisResult> explicitly to the streamed component

Also: StreamedGraphSection ignores error from useStreamableValue() entirely, so failures will silently render a blank/partial card.

Suggestion

Replace the heuristic + any with proper narrowing and an error state. Example:

function isDataAnalysisResult(v: unknown): v is DataAnalysisResult {
  return !!v && typeof v === 'object' && 'chartType' in (v as any) && 'data' in (v as any)
}

export function GraphSection({ result }: GraphSectionProps) {
  if (!result) return null
  if (typeof result === 'string' || isDataAnalysisResult(result)) {
    return <GraphCard data={result} />
  }
  return <StreamedGraphSection result={result} />
}

function StreamedGraphSection({ result }: { result: StreamableValue<DataAnalysisResult> }) {
  const [data, error, pending] = useStreamableValue(result)
  if (error) return <div className="text-sm text-destructive">Failed to load chart.</div>
  ...
}

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that removes the any and adds streamed error handling.


function GraphCard({ data, pending }: { data: any, pending?: boolean }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

pending prop is declared but never passed.

GraphCard accepts an optional pending prop, but neither GraphSection nor StreamedGraphSection passes it. Remove it to avoid confusion.

♻️ Proposed fix
-function GraphCard({ data, pending }: { data: any, pending?: boolean }) {
+function GraphCard({ data }: { data: any }) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function GraphCard({ data, pending }: { data: any, pending?: boolean }) {
function GraphCard({ data }: { data: any }) {
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` at line 70, GraphCard declares an unused
optional prop pending; remove pending from the component props to avoid dead/
confusing API surface. Edit the GraphCard function signature (remove pending
from the destructured props and its type annotation), delete any internal
references to pending inside GraphCard, and update any related PropTypes/TS
types if present; no changes are needed in callers (GraphSection,
StreamedGraphSection) since they never pass pending.

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 <div className="flex items-center justify-center h-full text-muted-foreground italic">Missing chart data or configuration</div>;
Comment on lines +88 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating config.series before entering chart-specific branches.

For bar, line, and area charts, if config.series is undefined or empty, the optional chaining (config.series?.map(...)) silently renders a chart with axes but no data series — a confusing empty chart with no feedback. A check here would cover all chart types uniformly.

♻️ Suggested addition after the existing guard
     if (!plotData || !config) return <div className="flex items-center justify-center h-full text-muted-foreground italic">Missing chart data or configuration</div>;
+    if (!config.series?.length) return <div className="flex items-center justify-center h-full text-muted-foreground italic">No data series configured</div>;
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 88 - 89, The renderChart function
currently only checks for plotData and config but doesn’t validate
config.series, so bar/line/area branches using config.series?.map(...) can
render empty charts; update renderChart to check that config.series is defined
and has length > 0 before entering chart-specific branches (e.g., where 'bar',
'line', 'area' are handled) and return the same "Missing chart data or
configuration" (or a similar user-facing message) when series is missing or
empty to avoid rendering confusing empty charts.


switch (chartType) {
case 'bar':
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={plotData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={config.xAxisKey} />
<YAxis />
<Tooltip />
<Legend />
{config.series?.map((s, i) => (
<Bar key={s.key} dataKey={s.key} name={s.name} fill={s.color || COLORS[i % COLORS.length]} />
))}
</BarChart>
</ResponsiveContainer>
);
case 'line':
return (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={plotData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={config.xAxisKey} />
<YAxis />
<Tooltip />
<Legend />
{config.series?.map((s, i) => (
<Line key={s.key} type="monotone" dataKey={s.key} name={s.name} stroke={s.color || COLORS[i % COLORS.length]} />
))}
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={plotData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={config.xAxisKey} />
<YAxis />
<Tooltip />
<Legend />
{config.series?.map((s, i) => (
<Area key={s.key} type="monotone" dataKey={s.key} name={s.name} stroke={s.color || COLORS[i % COLORS.length]} fill={s.color || COLORS[i % COLORS.length]} />
))}
</AreaChart>
</ResponsiveContainer>
);
case 'pie':
return (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={plotData}
dataKey={config.series?.[0]?.key}
nameKey={config.xAxisKey}
cx="50%"
cy="50%"
outerRadius={80}
label
>
{plotData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
);
Comment on lines +137 to +158
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pie chart will crash at runtime if plotData is not a proper array.

Line 89 guards against falsy plotData, but if plotData is a truthy non-array (e.g., an object), plotData.map() on line 150 throws a TypeError. Unlike bar/line/area where plotData is only passed as a prop to Recharts (which handles it gracefully), the pie branch calls .map() directly.

Additionally, the previously flagged issue about config.series?.[0]?.key being undefined when series is empty still applies.

🛡️ Proposed guard
       case 'pie':
+        if (!Array.isArray(plotData)) return <div className="flex items-center justify-center h-full text-muted-foreground italic">Invalid data for pie chart</div>;
+        if (!config.series?.length) return <div className="flex items-center justify-center h-full text-muted-foreground italic">Missing series configuration for pie chart</div>;
         return (
           <ResponsiveContainer width="100%" height="100%">
             <PieChart>
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 137 - 158, The Pie branch in
components/graph-section.tsx directly calls plotData.map which will throw if
plotData is a truthy non-array; change the rendering to first normalize/guard
plotData (e.g., const safePlotData = Array.isArray(plotData) ? plotData : [])
and use safePlotData for mapping so map is never called on a non-array, and also
guard the dataKey used in <Pie> (config.series?.[0]?.key) by computing a const
dataKey = config.series?.[0]?.key and either provide a sensible fallback or skip
rendering the Pie/Cell elements when dataKey is undefined to avoid passing
undefined to Recharts.

case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%">
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" dataKey={config.xAxisKey} name={config.xAxisKey} />
<YAxis type="number" dataKey={config.yAxisKey} name={config.yAxisKey} />
<Tooltip cursor={{ strokeDasharray: '3 3' }} />
<Legend />
{config.series?.map((s, i) => (
<Scatter key={s.key} name={s.name} data={plotData} fill={s.color || COLORS[i % COLORS.length]} />
))}
</ScatterChart>
</ResponsiveContainer>
);
Comment on lines +159 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scatter chart renders the same plotData for every series, producing duplicate overlapping points.

Each <Scatter> receives data={plotData} (the full dataset). For multi-series scatter, each series should have its own subset of data or a distinct Y-axis key. Currently, adding multiple series entries just draws identical scatter dots with different colors on top of each other.

🐛 Proposed fix — use `dataKey` per series instead of duplicating the entire dataset

If the intent is that all data points share the same X values but have different Y keys per series:

             <ScatterChart>
               <CartesianGrid strokeDasharray="3 3" />
               <XAxis type="number" dataKey={config.xAxisKey} name={config.xAxisKey} />
-              <YAxis type="number" dataKey={config.yAxisKey} name={config.yAxisKey} />
+              <YAxis type="number" name="value" />
               <Tooltip cursor={{ strokeDasharray: '3 3' }} />
               <Legend />
               {config.series?.map((s, i) => (
-                <Scatter key={s.key} name={s.name} data={plotData} fill={s.color || COLORS[i % COLORS.length]} />
+                <Scatter key={s.key} name={s.name} data={plotData.map(d => ({ [config.xAxisKey]: d[config.xAxisKey], [s.key]: d[s.key] }))} fill={s.color || COLORS[i % COLORS.length]} dataKey={s.key} />
               ))}
             </ScatterChart>

Alternatively, if there's always a single series, guard with a single <Scatter> instead of mapping.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%">
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" dataKey={config.xAxisKey} name={config.xAxisKey} />
<YAxis type="number" dataKey={config.yAxisKey} name={config.yAxisKey} />
<Tooltip cursor={{ strokeDasharray: '3 3' }} />
<Legend />
{config.series?.map((s, i) => (
<Scatter key={s.key} name={s.name} data={plotData} fill={s.color || COLORS[i % COLORS.length]} />
))}
</ScatterChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%">
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" dataKey={config.xAxisKey} name={config.xAxisKey} />
<YAxis type="number" name="value" />
<Tooltip cursor={{ strokeDasharray: '3 3' }} />
<Legend />
{config.series?.map((s, i) => (
<Scatter key={s.key} name={s.name} data={plotData.map(d => ({ [config.xAxisKey]: d[config.xAxisKey], [s.key]: d[s.key] }))} fill={s.color || COLORS[i % COLORS.length]} dataKey={s.key} />
))}
</ScatterChart>
</ResponsiveContainer>
);
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 159 - 173, The scatter case
currently renders identical points because each <Scatter> uses data={plotData};
change the mapping so each series uses its own dataKey or per-series dataset:
inside the map over config.series, either pass dataKey={s.key} (and ensure XAxis
uses dataKey={config.xAxisKey}) or compute seriesData = plotData.map(r => ({ x:
r[config.xAxisKey], y: r[s.key] })) and pass data={seriesData} with XAxis/YAxis
configured to use "x"/"y"; alternatively, if config.series length === 1, render
a single <Scatter> instead of mapping. Update the <Scatter> elements (symbol:
Scatter, config.series, plotData, config.xAxisKey) accordingly.

default:
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
Unsupported chart type: {chartType || 'None'}
</div>
);
}
};

return (
<Section className="py-2">
<div className="mb-2">
<ToolBadge tool="dataAnalysis">Graph: {title || 'Untitled'}</ToolBadge>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg font-medium">{title || 'Data Analysis'}</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent>
<div className="h-[300px] w-full">
{renderChart()}
</div>
</CardContent>
</Card>
</Section>
);
}
1 change: 0 additions & 1 deletion components/header-search-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ export function HeaderSearchButton() {

formData.append('action', 'resolution_search')
formData.append('timezone', mapData.currentTimezone || 'UTC')
formData.append('drawnFeatures', JSON.stringify(mapData.drawnFeatures || []))

const center = mapProvider === 'mapbox' && map ? map.getCenter() : mapData.cameraState?.center;
if (center) {
Expand Down
23 changes: 19 additions & 4 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis
3. **Search Specificity:** When using the 'search' tool, formulate queries that are as specific as possible.
4. **Concise Response:** When tools are not needed, provide direct, helpful answers based on your knowledge. Match the user's language.
5. **Citations:** Always cite source URLs when using information from tools.
6. **No Text-Based Charts:** NEVER create charts, graphs, or visual representations using text-based formatting, asterisks (*), or other hardcoded characters. ALWAYS use the \`dataAnalysis\` tool for any visual data representation.

### **Tool Usage Guidelines (Mandatory)**

Expand All @@ -47,7 +48,20 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis
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.
- **Mandatory**: You MUST use this tool for all charts and graphs. NEVER attempt to create a chart using text, asterisks, or any other manual formatting in your response.
- **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
Expand All @@ -68,9 +82,10 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis

#### **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.

Expand Down
4 changes: 2 additions & 2 deletions lib/agents/resolution-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ Analyze the user's prompt and the image to provide a holistic understanding of t
const filteredMessages = messages.filter(msg => msg.role !== 'system');

// Check if any message contains an image (resolution search is specifically for image analysis)
const hasImage = messages.some((message: any) =>
const hasImage = messages.some((message: any) =>
Array.isArray(message.content) &&
message.content.some((part: any) => part.type === 'image')
)

// Use streamObject to get partial results.
// Use streamObject to get the partial object stream.
return streamObject({
model: await getModel(hasImage),
system: systemPrompt,
Expand Down
19 changes: 19 additions & 0 deletions lib/agents/tools/data-analysis.tsx
Original file line number Diff line number Diff line change
@@ -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<DataAnalysisResult>()

uiStream.append(<GraphSection result={streamResults.value} />)

streamResults.done(result)
Comment on lines +10 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

createStreamableValue is created and immediately .done() — streaming is unused.

The StreamableValue is resolved synchronously in the same tick. This adds overhead without benefit. If the result is already fully available in execute, pass it directly as a static object.

♻️ Simplified version
   execute: async (result: DataAnalysisResult) => {
-    const streamResults = createStreamableValue<DataAnalysisResult>()
-
-    uiStream.append(<GraphSection result={streamResults.value} />)
-
-    streamResults.done(result)
+    uiStream.append(<GraphSection result={result} />)
 
     return result
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
execute: async (result: DataAnalysisResult) => {
const streamResults = createStreamableValue<DataAnalysisResult>()
uiStream.append(<GraphSection result={streamResults.value} />)
streamResults.done(result)
execute: async (result: DataAnalysisResult) => {
uiStream.append(<GraphSection result={result} />)
return result
}
🤖 Prompt for AI Agents
In `@lib/agents/tools/data-analysis.tsx` around lines 10 - 15, The current execute
implementation creates a StreamableValue via
createStreamableValue<DataAnalysisResult>(), immediately calls
streamResults.done(result) and then renders <GraphSection
result={streamResults.value} />, which wastes a streaming abstraction; instead,
pass the result directly to the UI. Replace the
createStreamableValue/streamResults usage in the execute method by calling
uiStream.append(<GraphSection result={result} />) (keep types:
DataAnalysisResult) and remove the unused streamResults/done flow so
GraphSection receives the static object synchronously.


return result
}
Comment on lines +7 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Double rendering: this tool AND processEvents in app/actions.tsx both append a GraphSection for the same tool call.

This tool's execute appends a GraphSection to uiStream (line 13). Simultaneously, processEvents in app/actions.tsx (lines 345–350) also appends a GraphSection when it encounters a dataAnalysis tool-call event. The user will see two identical charts for every data analysis invocation.

Pick one rendering site and remove the other. Since the tool's execute already handles UI rendering (the standard pattern for AI RSC tools), remove the duplicate in app/actions.tsx:

 // app/actions.tsx – inside processEvents
        } else if (event.type === 'tool-call') {
-         if (event.toolName === 'dataAnalysis') {
-           uiStream.append(
-             <Section title="Analysis">
-               <GraphSection result={event.args as any} />
-             </Section>
-           )
-         }
        }
🤖 Prompt for AI Agents
In `@lib/agents/tools/data-analysis.tsx` around lines 7 - 18, The
dataAnalysisTool.execute already appends a GraphSection to the uiStream, so
remove the duplicate append in the processEvents handler that reacts to a
dataAnalysis tool-call (the code path that currently creates/attaches
GraphSection for tool "dataAnalysis"); keep any other logic in processEvents for
event handling but delete the GraphSection/uiStream.append call there so only
dataAnalysisTool.execute renders the chart.

Comment on lines +7 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dataAnalysis tool appends a <GraphSection /> from within the tool execution itself, but app/actions.tsx also appends a graph UI on the tool-call event.

Depending on how researcher is implemented (and whether tool execution also appends UI), you can end up with duplicate charts for the same tool call: one appended when the call is emitted, and another appended when the tool finishes.

This is a design/flow issue: choose one place to render tool UI (either in the tool execute or in the outer event loop), not both.

Suggestion

Pick a single rendering strategy:

  • Preferred: render the UI inside the tool execute() (since it has the validated result), and remove the tool-call rendering branch in app/actions.tsx.
  • Alternatively: keep the outer tool-call handler and remove UI appending from the tool.

If you want, I can add a commit implementing the preferred approach (remove tool-call chart appending and validate via schema in the tool)—reply with "@CharlieHelps yes please".

})
5 changes: 5 additions & 0 deletions lib/agents/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -25,6 +26,10 @@ export const getTools = ({ uiStream, fullResponse, mapProvider }: ToolProps) =>
geospatialQueryTool: geospatialTool({
uiStream,
mapProvider
}),
dataAnalysis: dataAnalysisTool({
uiStream,
fullResponse
})
}

Expand Down
2 changes: 2 additions & 0 deletions lib/agents/writer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export async function writer(
Link format: [link text](url)
Image format: ![alt text](url)

**IMPORTANT**: NEVER create charts, graphs, or visual representations using text-based formatting, asterisks (*), or other hardcoded characters. If you need to present data visually, ensure the appropriate tool has been used. Do not attempt to simulate a chart in your text response.

There are also some proconfigured example queires.
When asked about 'What is a planet computer?' answer with the following: '"A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet'
Comment on lines +28 to 31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typos in system prompt.

Line 30: "proconfigured example queires" → "preconfigured example queries".

Proposed fix
-    There are also some proconfigured example queires. 
+    There are also some preconfigured example queries. 
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**IMPORTANT**: NEVER create charts, graphs, or visual representations using text-based formatting, asterisks (*), or other hardcoded characters. If you need to present data visually, ensure the appropriate tool has been used. Do not attempt to simulate a chart in your text response.
There are also some proconfigured example queires.
When asked about 'What is a planet computer?' answer with the following: '"A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet'
**IMPORTANT**: NEVER create charts, graphs, or visual representations using text-based formatting, asterisks (*), or other hardcoded characters. If you need to present data visually, ensure the appropriate tool has been used. Do not attempt to simulate a chart in your text response.
There are also some preconfigured example queries.
When asked about 'What is a planet computer?' answer with the following: '"A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet'
🤖 Prompt for AI Agents
In `@lib/agents/writer.tsx` around lines 28 - 31, In the system prompt text in
lib/agents/writer.tsx replace the typo'd phrase "proconfigured example queires"
with "preconfigured example queries" (locate the string that currently reads
'**IMPORTANT**: NEVER create charts...' and update that sentence); ensure you
only correct the wording and preserve surrounding punctuation and the example
query content including the planet computer sentence.

`;
Expand Down
25 changes: 25 additions & 0 deletions lib/schema/data-analysis.tsx
Original file line number Diff line number Diff line change
@@ -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'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

data field typing is inconsistent with DataAnalysisResult in lib/types/index.ts.

The schema defines data as z.array(z.record(z.any())), which infers to Record<string, any>[]. However, DataAnalysisResult.data in lib/types/index.ts (Line 32) is typed as any[]. These should be consistent — the schema is stricter (records), which is correct. Update the type in lib/types/index.ts to match:

Suggested fix in lib/types/index.ts
-  data: any[];
+  data: Record<string, any>[];
🤖 Prompt for AI Agents
In `@lib/schema/data-analysis.tsx` at line 8, The DataAnalysisResult type's data
property is currently any[] but must match the schema's stricter shape; update
the DataAnalysisResult interface/type so its data field is typed as
Record<string, any>[] (or equivalent zod-inferred shape) instead of any[],
ensuring consistency with the schema's data: z.array(z.record(z.any())).
Reference the DataAnalysisResult type and the data property when making this
change.

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'),
Comment on lines +9 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataAnalysisSchema defines series as required (z.array(...)), but GraphCard treats config.series as optional (config.series?.map). That mismatch tends to hide broken tool outputs and can lead to empty charts without an obvious error state.

If the schema truly requires series, the UI should assume it exists and render a validation error when it doesn’t. If it’s optional, the schema should reflect that.

Suggestion

Align schema and renderer:

  • If series is required, remove optional chaining and show a clear message when series.length === 0.
  • If series can be omitted (e.g., pie charts), make it optional in the schema or model it as a discriminated union by chartType.

Example quick fix:

series: z.array(...).min(1, 'At least one series is required')

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

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<typeof dataAnalysisSchema>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: PartialDataAnalysis wraps the Zod schema object, not the inferred data type.

typeof dataAnalysisSchema resolves to the Zod schema class (ZodObject<...>), not the inferred TypeScript data shape. DeepPartial of a Zod schema object will produce a nonsensical type. You need z.infer<> to extract the data shape first.

Proposed fix
-export type PartialDataAnalysis = DeepPartial<typeof dataAnalysisSchema>
+export type PartialDataAnalysis = DeepPartial<z.infer<typeof dataAnalysisSchema>>
🤖 Prompt for AI Agents
In `@lib/schema/data-analysis.tsx` at line 25, PartialDataAnalysis currently
applies DeepPartial to the Zod schema object rather than the inferred data
shape; change its definition to apply DeepPartial to the inferred type from
dataAnalysisSchema (i.e., use z.infer<typeof dataAnalysisSchema>), and ensure z
is imported if not already; update the type alias PartialDataAnalysis
accordingly so it becomes DeepPartial<z.infer<typeof dataAnalysisSchema>>.

21 changes: 21 additions & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"react-markdown": "^9.1.0",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^10.0.6",
"recharts": "^3.7.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

recharts 3.7.0 npm React 19 compatibility

💡 Result:

Recharts 3.7.0 is npm-install compatible with React 19: its peerDependencies explicitly allow react, react-dom, and react-is ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0. [1]

Practical note: you should ensure react-is matches your React major (so with React 19, use react-is@^19), as mismatches have historically caused “charts don’t render” problems in React 19 setups. [1][2]

Sources:
[1] Recharts 3.7.0 package.json (jsDelivr CDN) https://cdn.jsdelivr.net/npm/recharts@3.7.0/package.json
[2] Recharts GitHub issue: “Support React 19” #4558 recharts/recharts#4558


🌐 Web query:

recharts version 3.7.0 release peer dependencies

💡 Result:

Recharts v3.7.0 (released Jan 2026) (newreleases.io) has these peer dependencies:

(Upstream discussion notes react-dom + react-is are kept as peers to ensure they match your installed React version.) (github.com)

Citations:


Recharts 3.7.0 is compatible with React 19 — dependency is safe to add.

Recharts 3.7.0 (released Jan 2026) explicitly supports React 19 in its peer dependencies (^19.0.0). Ensure react-is matches your React major version (React 19 requires react-is@^19) to avoid rendering issues, as mismatches have historically caused problems in React 19 setups.

🤖 Prompt for AI Agents
In `@package.json` at line 89, The package.json currently adds "recharts":
"^3.7.0" which is compatible with React 19, but you must also ensure the peer
dependency for react-is matches your React major version to avoid runtime
rendering issues; update package.json so "react-is" is set to the React
19-compatible range (e.g., "react-is": "^19") in dependencies or devDependencies
and run install to resolve and lock the correct version, keeping the existing
"recharts": "^3.7.0" entry.

"rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
Expand Down
Binary file added verification/fix_verification.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.