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
57 changes: 35 additions & 22 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { FeatureCollection } from 'geojson'
import { Spinner } from '@/components/ui/spinner'
import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch } from '@/lib/agents'
// Removed import of useGeospatialToolMcp as it no longer exists and was incorrectly used here.
// The geospatialTool (if used by agents like researcher) now manages its own MCP client.
import { writer } from '@/lib/agents/writer'
Expand All @@ -27,6 +27,7 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler' // Add this import
import { GraphSection } from '@/components/graph-section'

// Define the type for related queries
type RelatedQueries = {
Expand All @@ -46,14 +47,6 @@ async function submit(formData?: FormData, skip?: boolean) {
if (action === 'resolution_search') {
const file = formData?.get('file') as File;
const timezone = (formData?.get('timezone') as string) || 'UTC';
const drawnFeaturesString = formData?.get('drawnFeatures') as string;
let drawnFeatures: DrawnFeature[] = [];
try {
drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : [];
} catch (e) {
console.error('Failed to parse drawnFeatures:', e);
}

if (!file) {
throw new Error('No file provided for resolution search.');
}
Expand Down Expand Up @@ -95,18 +88,8 @@ async function submit(formData?: FormData, skip?: boolean) {

async function processResolutionSearch() {
try {
// Call the simplified agent, which now returns a stream.
const streamResult = await resolutionSearch(messages, timezone, drawnFeatures);

let fullSummary = '';
for await (const partialObject of streamResult.partialObjectStream) {
if (partialObject.summary) {
fullSummary = partialObject.summary;
summaryStream.update(fullSummary);
}
}

const analysisResult = await streamResult.object;
// Call the simplified agent, which now returns data directly.
const analysisResult = await resolutionSearch(messages, timezone) as any;

// Mark the summary stream as done with the result.
summaryStream.done(analysisResult.summary || 'Analysis complete.');
Expand Down Expand Up @@ -315,7 +298,11 @@ async function submit(formData?: FormData, skip?: boolean) {
image: dataUrl,
mimeType: file.type
})
} else if (file.type === 'text/plain') {
} else if (
file.type === 'text/plain' ||
file.type === 'text/csv' ||
file.type === 'application/json'
) {
Comment on lines +301 to +305
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

Add a server-side file size guard for csv/json uploads.

Client-side checks are not sufficient; a malicious request can bypass them and force large memory allocations on the server.

🔧 Proposed fix
-  if (file) {
-    const buffer = await file.arrayBuffer()
+  if (file) {
+    const MAX_FILE_SIZE = 10 * 1024 * 1024
+    if (file.size > MAX_FILE_SIZE) {
+      throw new Error('File size must be less than 10MB')
+    }
+    const buffer = await file.arrayBuffer()
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 301 - 305, Add a server-side size guard for
uploads matching the file.type checks ('text/plain', 'text/csv',
'application/json') to prevent large-memory malicious uploads: define a
MAX_UPLOAD_BYTES constant and in the same handler where file.type is inspected
(the branch using file.type === 'text/plain' || 'text/csv' ||
'application/json') reject the request if file.size (or a streamed byte counter
if using streams) exceeds that limit, returning/throwing an appropriate error
before attempting to parse or buffer the file; ensure the guard runs
unconditionally on the server side so client-side checks cannot be bypassed.

const textContent = Buffer.from(buffer).toString('utf-8')
const existingTextPart = messageParts.find(p => p.type === 'text')
if (existingTextPart) {
Expand Down Expand Up @@ -762,6 +749,32 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
),
isCollapsed: isCollapsed.value
}
case 'dataAnalysis':
return {
id,
component: (
<>
<GraphSection result={searchResults.value} />
{toolOutput.geospatial && toolOutput.geospatial.length > 0 && (
<MapQueryHandler
toolOutput={{
type: 'MAP_QUERY_TRIGGER',
originalUserInput: JSON.stringify(toolOutput.geospatial[0]),
timestamp: new Date().toISOString(),
mcp_response: {
location: {
latitude: toolOutput.geospatial[0].latitude,
longitude: toolOutput.geospatial[0].longitude,
place_name: toolOutput.geospatial[0].label
}
}
}}
/>
)}
Comment on lines +752 to +773

Choose a reason for hiding this comment

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

This constructs a MAP_QUERY_TRIGGER with originalUserInput: JSON.stringify(toolOutput.geospatial[0]) and uses only the first geospatial point. If multiple points are returned, the map will ignore all but one.

Also, this code is doing a lot of ad-hoc shaping of a map tool payload inside UI-state mapping logic, which makes the contract fragile and hard to evolve.

Suggestion

At minimum, consider passing the entire geospatial array through and letting MapQueryHandler decide what to do, or explicitly document that only the first point is supported.

If MapQueryHandler expects a single location, add a small helper function (in this file) to build the payload so the shape is centralized and testable.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that factors payload creation into a helper and (optionally) uses the first item explicitly with a comment.

Comment on lines +758 to +773
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

Add defensive checks for geospatial item properties.

The code accesses toolOutput.geospatial[0].latitude, .longitude, and .label without validating these properties exist. If the geospatial array contains a malformed item, undefined values will be passed to MapQueryHandler.

🛡️ Proposed defensive check
-                      {toolOutput.geospatial && toolOutput.geospatial.length > 0 && (
+                      {toolOutput.geospatial &&
+                        toolOutput.geospatial.length > 0 &&
+                        toolOutput.geospatial[0].latitude != null &&
+                        toolOutput.geospatial[0].longitude != null && (
                         <MapQueryHandler
                           toolOutput={{
                             type: 'MAP_QUERY_TRIGGER',
                             originalUserInput: JSON.stringify(toolOutput.geospatial[0]),
                             timestamp: new Date().toISOString(),
                             mcp_response: {
                               location: {
                                 latitude: toolOutput.geospatial[0].latitude,
                                 longitude: toolOutput.geospatial[0].longitude,
-                                place_name: toolOutput.geospatial[0].label
+                                place_name: toolOutput.geospatial[0].label ?? ''
                               }
                             }
                           }}
                         />
                       )}
📝 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
{toolOutput.geospatial && toolOutput.geospatial.length > 0 && (
<MapQueryHandler
toolOutput={{
type: 'MAP_QUERY_TRIGGER',
originalUserInput: JSON.stringify(toolOutput.geospatial[0]),
timestamp: new Date().toISOString(),
mcp_response: {
location: {
latitude: toolOutput.geospatial[0].latitude,
longitude: toolOutput.geospatial[0].longitude,
place_name: toolOutput.geospatial[0].label
}
}
}}
/>
)}
{toolOutput.geospatial &&
toolOutput.geospatial.length > 0 &&
toolOutput.geospatial[0].latitude != null &&
toolOutput.geospatial[0].longitude != null && (
<MapQueryHandler
toolOutput={{
type: 'MAP_QUERY_TRIGGER',
originalUserInput: JSON.stringify(toolOutput.geospatial[0]),
timestamp: new Date().toISOString(),
mcp_response: {
location: {
latitude: toolOutput.geospatial[0].latitude,
longitude: toolOutput.geospatial[0].longitude,
place_name: toolOutput.geospatial[0].label ?? ''
}
}
}}
/>
)}
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 772 - 787, The code passes potentially
undefined latitude/longitude/label from toolOutput.geospatial[0] into
MapQueryHandler; add defensive checks before constructing the MAP_QUERY_TRIGGER
payload: verify toolOutput.geospatial exists and is a non-empty array, and
validate that toolOutput.geospatial[0] has numeric latitude and longitude and a
non-empty label (or provide safe defaults or skip rendering). Update the
conditional around MapQueryHandler (referencing toolOutput.geospatial and
MapQueryHandler) to only render when these validated properties are present, or
transform the values to safe fallbacks before passing them into the
mcp_response.location fields.

</>
),
isCollapsed: isCollapsed.value
}
Comment on lines +752 to +777
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

🏁 Script executed:

#!/bin/bash
# Find the DataAnalysisResult type definition
echo "=== Searching for DataAnalysisResult type ==="
rg -n -A 20 'type DataAnalysisResult|DataAnalysisResult = ' --type=ts -g '!node_modules' | head -100

echo -e "\n=== Searching for schema definitions ==="
rg -n -B 2 -A 15 'dataAnalysisSchema' --type=ts -g '!node_modules' | head -150

echo -e "\n=== Checking app/actions.tsx context (around line 766) ==="
wc -l app/actions.tsx

Repository: QueueLab/QCX

Length of output: 4460


🏁 Script executed:

#!/bin/bash
echo "=== Reading app/actions.tsx around line 766 ==="
sed -n '740,810p' app/actions.tsx | cat -n

echo -e "\n=== Checking broader context (line 700-750) ==="
sed -n '700,750p' app/actions.tsx | cat -n

Repository: QueueLab/QCX

Length of output: 5172


Add runtime type validation for toolOutput in the dataAnalysis case.

The toolOutput is parsed from JSON without schema validation. While a DataAnalysisResult type and dataAnalysisSchema already exist (in lib/types and lib/schema/data-analysis.tsx), they are only used in the tool definition. Here, the code accesses nested properties like toolOutput.geospatial[0].latitude without confirming the structure at runtime. Consider reusing dataAnalysisSchema with parse() to validate before access, or add explicit type guards for the geospatial property structure.

🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 766 - 791, The dataAnalysis branch reads
toolOutput and accesses nested fields without runtime validation; use the
existing dataAnalysisSchema (from lib/schema/data-analysis) to validate/parse
toolOutput (e.g., dataAnalysisSchema.parse or safeParse) into a typed
DataAnalysisResult before rendering, then conditionally render MapQueryHandler
only when the parsed result has a non-empty geospatial array and use
parsed.geospatial[0].latitude/longitude/label for the mcp_response;
alternatively add explicit type guards for toolOutput.geospatial and its
elements to avoid direct indexing of potentially invalid data.

default:
console.warn(
`Unhandled tool result in getUIStateFromAIState: ${name}`
Expand Down
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 @@ -214,7 +214,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
272 changes: 272 additions & 0 deletions components/graph-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
'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']

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} />;
}

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 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 <div className="flex items-center justify-center h-full text-muted-foreground italic">Missing chart data or configuration</div>;

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 (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={plotData} margin={{ top: 10, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke={themeColors.grid} vertical={false} />
<XAxis
dataKey={config.xAxisKey}
{...commonAxisProps}
dy={10}
/>
<YAxis {...commonAxisProps} />
<Tooltip
contentStyle={{
backgroundColor: themeColors.tooltip.bg,
color: themeColors.tooltip.text,
borderColor: themeColors.tooltip.border,
borderRadius: '8px'
}}
/>
<Legend wrapperStyle={{ paddingTop: '20px' }} />
{config.series?.map((s, i) => (
<Bar key={s.key} dataKey={s.key} name={s.name} fill={s.color || COLORS[i % COLORS.length]} radius={[4, 4, 0, 0]} />
))}
</BarChart>
</ResponsiveContainer>
);
case 'line':
return (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={plotData} margin={{ top: 10, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke={themeColors.grid} vertical={false} />
<XAxis
dataKey={config.xAxisKey}
{...commonAxisProps}
dy={10}
/>
<YAxis {...commonAxisProps} />
<Tooltip
contentStyle={{
backgroundColor: themeColors.tooltip.bg,
color: themeColors.tooltip.text,
borderColor: themeColors.tooltip.border,
borderRadius: '8px'
}}
/>
<Legend wrapperStyle={{ paddingTop: '20px' }} />
{config.series?.map((s, i) => (
<Line key={s.key} type="monotone" dataKey={s.key} name={s.name} stroke={s.color || COLORS[i % COLORS.length]} strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
))}
</LineChart>
</ResponsiveContainer>
);
case 'area':
return (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={plotData} margin={{ top: 10, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke={themeColors.grid} vertical={false} />
<XAxis
dataKey={config.xAxisKey}
{...commonAxisProps}
dy={10}
/>
<YAxis {...commonAxisProps} />
<Tooltip
contentStyle={{
backgroundColor: themeColors.tooltip.bg,
color: themeColors.tooltip.text,
borderColor: themeColors.tooltip.border,
borderRadius: '8px'
}}
/>
<Legend wrapperStyle={{ paddingTop: '20px' }} />
{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]} fillOpacity={0.3} />
))}
</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={{ fill: themeColors.text, fontSize: 12 }}
>
{plotData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: themeColors.tooltip.bg,
color: themeColors.tooltip.text,
borderColor: themeColors.tooltip.border,
borderRadius: '8px'
}}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
);
case 'scatter':
return (
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 10, right: 10, left: -20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke={themeColors.grid} />
<XAxis
type="number"
dataKey={config.xAxisKey}
name={config.xAxisKey}
{...commonAxisProps}
dy={10}
/>
<YAxis type="number" dataKey={config.yAxisKey} name={config.yAxisKey} {...commonAxisProps} />
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
contentStyle={{
backgroundColor: themeColors.tooltip.bg,
color: themeColors.tooltip.text,
borderColor: themeColors.tooltip.border,
borderRadius: '8px'
}}
/>
<Legend wrapperStyle={{ paddingTop: '20px' }} />
{config.series?.map((s, i) => (
<Scatter key={s.key} name={s.name} data={plotData} fill={s.color || COLORS[i % COLORS.length]} />
))}
Comment on lines +29 to +241
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

🧩 Analysis chain

🏁 Script executed:

# First, explore the repository structure to understand the theme setup
find . -type f -name "*.css" -o -name "*.ts" -o -name "*.tsx" | head -20
git ls-files | grep -E "(theme|global|config)" | head -20

Repository: QueueLab/QCX

Length of output: 818


🏁 Script executed:

# Search for CSS variables definition - common locations
find . -type f \( -name "*.css" -o -name "globals.css" -o -name "*.scss" \) | xargs grep -l "primary\|secondary\|accent\|muted" 2>/dev/null | head -10

Repository: QueueLab/QCX

Length of output: 73


🏁 Script executed:

# Check if --primary, --secondary, --accent, --muted CSS variables are defined
rg "--primary|--secondary|--accent|--muted" --type css --type tsx --type ts | head -30

Repository: QueueLab/QCX

Length of output: 496


🏁 Script executed:

# Verify the exact COLORS usage in graph-section.tsx
rg "COLORS" components/graph-section.tsx -A 2 -B 2

Repository: QueueLab/QCX

Length of output: 1666


🏁 Script executed:

# Examine globals.css for CSS variables
cat app/globals.css | head -100

Repository: QueueLab/QCX

Length of output: 2468


🏁 Script executed:

# Check tailwind.config.ts for theme colors
cat tailwind.config.ts

Repository: QueueLab/QCX

Length of output: 2482


🏁 Script executed:

# Search for primary, secondary, accent, muted in CSS variables (correct rg syntax)
rg "primary|secondary|accent|muted" --type css -C 3

Repository: QueueLab/QCX

Length of output: 3378


Use theme-aware series colors instead of hardcoded palette.

The COLORS array is static and won't adapt to light/dark/earth themes. Extract --primary, --secondary, --accent, and --muted from CSS variables, convert them to hex (since Recharts requires hex for SVG), and apply them to series rendering across all chart types.

🎨 Proposed fix (theme palette + hex conversion)
-const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d']
+const FALLBACK_COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d']
+const THEME_COLOR_VARS = ['--primary', '--secondary', '--accent', '--muted']
+
+const hslToHex = (hsl: string) => {
+  const [h, s, l] = hsl.replace(/%/g, '').split(/\s+/).map(Number)
+  if ([h, s, l].some(Number.isNaN)) return ''
+  const sN = s / 100
+  const lN = l / 100
+  const c = (1 - Math.abs(2 * lN - 1)) * sN
+  const x = c * (1 - Math.abs((h / 60) % 2 - 1))
+  const m = lN - c / 2
+  let [r, g, b] = [0, 0, 0]
+  if (h < 60) [r, g, b] = [c, x, 0]
+  else if (h < 120) [r, g, b] = [x, c, 0]
+  else if (h < 180) [r, g, b] = [0, c, x]
+  else if (h < 240) [r, g, b] = [0, x, c]
+  else if (h < 300) [r, g, b] = [x, 0, c]
+  else [r, g, b] = [c, 0, x]
+  const toHex = (v: number) => Math.round((v + m) * 255).toString(16).padStart(2, '0')
+  return `#${toHex(r)}${toHex(g)}${toHex(b)}`
+}
 function GraphCard({ data, pending }: { data: any, pending?: boolean }) {
+  const [seriesColors, setSeriesColors] = React.useState(FALLBACK_COLORS)
+  React.useEffect(() => {
+    if (typeof window === 'undefined') return
+    const styles = getComputedStyle(document.documentElement)
+    const palette = THEME_COLOR_VARS
+      .map(v => hslToHex(styles.getPropertyValue(v).trim()))
+      .filter(Boolean)
+    if (palette.length) setSeriesColors(palette)
+  }, [])
-                <Bar key={s.key} dataKey={s.key} name={s.name} fill={s.color || COLORS[i % COLORS.length]} radius={[4, 4, 0, 0]} />
+                <Bar key={s.key} dataKey={s.key} name={s.name} fill={s.color || seriesColors[i % seriesColors.length]} radius={[4, 4, 0, 0]} />
-                <Line key={s.key} type="monotone" dataKey={s.key} name={s.name} stroke={s.color || COLORS[i % COLORS.length]} strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
+                <Line key={s.key} type="monotone" dataKey={s.key} name={s.name} stroke={s.color || seriesColors[i % seriesColors.length]} strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} />
-                <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]} fillOpacity={0.3} />
+                <Area key={s.key} type="monotone" dataKey={s.key} name={s.name} stroke={s.color || seriesColors[i % seriesColors.length]} fill={s.color || seriesColors[i % seriesColors.length]} fillOpacity={0.3} />
-                  <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
+                  <Cell key={`cell-${index}`} fill={seriesColors[index % seriesColors.length]} />
-                <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} fill={s.color || seriesColors[i % seriesColors.length]} />
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 29 - 241, Replace the static
COLORS palette with a theme-aware palette derived from CSS variables (e.g.,
--primary, --secondary, --accent, --muted) inside GraphCard (or top-level used
by GraphCard); read values via
getComputedStyle(document.documentElement).getPropertyValue, convert returned
CSS color strings to hex (handle hex, rgb(a), and hsl formats), build an array
like themePalette = [primaryHex, secondaryHex, accentHex, mutedHex] with a
fallback to the original COLORS, and then use s.color || themePalette[i %
themePalette.length] (and for Pie Cells) and for stroke/fill defaults across
Bar, Line, Area, Scatter and Cell renderings so charts adapt to light/dark/earth
themes while preserving existing fallbacks.

</ScatterChart>
Comment on lines +216 to +242
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

Guard missing yAxisKey for scatter charts.

yAxisKey is optional in the type but required for a scatter chart. Without it, the axis binds to undefined.

🛡️ Proposed fix
       case 'scatter':
+        if (!config.yAxisKey) {
+          return (
+            <div className="flex items-center justify-center h-full text-muted-foreground italic">
+              Missing y-axis key for scatter chart
+            </div>
+          )
+        }
         return (
🤖 Prompt for AI Agents
In `@components/graph-section.tsx` around lines 216 - 242, The scatter branch
currently assumes config.yAxisKey exists and binds YAxis to undefined; update
the 'scatter' case to validate that config.yAxisKey is present before rendering
the ScatterChart (e.g., early return a fallback/null or render an informative
placeholder/error UI), and ensure the YAxis and any bindings (YAxis dataKey and
Tooltip/XAxis names that reference config.yAxisKey) only use config.yAxisKey
when defined; locate the scatter rendering in the switch case (the JSX using
ScatterChart, XAxis, YAxis, Tooltip, and the map over config.series) and add the
guard/conditional rendering around it using config.yAxisKey to prevent binding
to undefined.

</ResponsiveContainer>
);
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>
);
}
2 changes: 1 addition & 1 deletion components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Loading