-
-
Notifications
You must be signed in to change notification settings - Fork 7
Add Generative Graphs UI and Data Analysis Tool #464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8c7120d
38704ca
37d772b
fdc7c5e
c6a615c
c5aae97
7fa9897
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const textContent = Buffer.from(buffer).toString('utf-8') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const existingTextPart = messageParts.find(p => p.type === 'text') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existingTextPart) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This constructs a 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. SuggestionAt minimum, consider passing the entire If 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add defensive checks for geospatial item properties. The code accesses 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isCollapsed: isCollapsed.value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+752
to
+777
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsxRepository: 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 -nRepository: QueueLab/QCX Length of output: 5172 Add runtime type validation for The 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Unhandled tool result in getUIStateFromAIState: ${name}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Large diffs are not rendered by default.
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -20Repository: 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 -10Repository: 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 -30Repository: 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 2Repository: QueueLab/QCX Length of output: 1666 🏁 Script executed: # Examine globals.css for CSS variables
cat app/globals.css | head -100Repository: QueueLab/QCX Length of output: 2468 🏁 Script executed: # Check tailwind.config.ts for theme colors
cat tailwind.config.tsRepository: 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 3Repository: QueueLab/QCX Length of output: 3378 Use theme-aware series colors instead of hardcoded palette. The 🎨 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 |
||
| </ScatterChart> | ||
|
Comment on lines
+216
to
+242
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard missing
🛡️ 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 |
||
| </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> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents