fix(chart): rewrite suggestion logic with better error handling
- Remove conflicting tool approach, use direct JSON - Add fallback: extract numbers from regex if AI fails - Add debug section to show what content was analyzed - Be more lenient: any 2+ numbers = valid chart - Better error messages - Add console logging for debugging Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,100 +73,132 @@ export async function POST(req: Request) {
|
||||
const sysConfig = await getSystemConfig()
|
||||
const { provider, model, timingInfo } = await resolveAiRouteWithTiming('suggest-charts', sysConfig)
|
||||
|
||||
// Build context for AI
|
||||
const toolContext = {
|
||||
userId,
|
||||
noteId,
|
||||
config: sysConfig,
|
||||
}
|
||||
|
||||
// Use the suggest_charts tool
|
||||
const suggestChartsTool = toolRegistry.get('suggest_charts')
|
||||
const tools = suggestChartsTool ? { suggest_charts: suggestChartsTool.buildTool(toolContext) } : {}
|
||||
|
||||
try {
|
||||
// 4. Call AI to analyze and suggest
|
||||
// 4. Call AI to analyze and suggest - direct JSON response (no tool)
|
||||
const { text } = await generateText({
|
||||
model: provider(model),
|
||||
system: `You are a data visualization assistant. Analyze the provided text and suggest appropriate chart types.
|
||||
|
||||
IMPORTANT: You MUST respond with valid JSON only. No markdown, no code blocks, no explanations.
|
||||
|
||||
DATA EXTRACTION RULES:
|
||||
- Extract ANY numerical data present in the text
|
||||
- Look for patterns like: "X: 123", "X = 123", "X is 123", "X (123)", "123X", "X $123", "123%"
|
||||
- Accept partial data (e.g., if only 2 values exist, that's still valid for a simple chart)
|
||||
- If fewer than 2 data points exist, return hasData=false with empty suggestions
|
||||
- Each suggestion must use the SAME extracted data (only chart type differs)
|
||||
- Return exactly 3 suggestions when data exists
|
||||
- Find ANY numbers in the text - even simple lists count
|
||||
- Look for: "X: 123", "X = 123", "123", "123%", "$123", etc.
|
||||
- If you find ANY 2+ numbers, create a chart
|
||||
- Be creative with labels if none exist (use "Item 1", "Item 2", etc.)
|
||||
- NEVER return hasData=false unless text is completely empty or has no numbers at all
|
||||
|
||||
CHART TYPE SELECTION:
|
||||
- bar: Comparing values across categories (default choice)
|
||||
- horizontal-bar: Categories with long labels
|
||||
- line: Time series or sequential data
|
||||
- area: Time series where magnitude matters
|
||||
- pie: Parts of a whole (percentages or proportions)
|
||||
- radar: Comparing multiple dimensions
|
||||
- funnel: Stages or conversion steps
|
||||
- gauge: Single value vs target
|
||||
CHART TYPES TO SUGGEST (always 3 different types):
|
||||
1. bar - for comparing categories
|
||||
2. line - for trends/sequences
|
||||
3. pie - for parts of whole
|
||||
|
||||
Response format (JSON):
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"type": "bar|horizontal-bar|line|area|pie|radar|funnel|gauge",
|
||||
"title": "Chart title",
|
||||
"data": [{"label": "Category", "value": 123}],
|
||||
"description": "What this chart shows",
|
||||
"rationale": "Why this chart type suits the data"
|
||||
}
|
||||
],
|
||||
"analyzedText": "Text that was analyzed",
|
||||
"detectedData": "Description of what data was found",
|
||||
"hasData": true
|
||||
}`,
|
||||
Response format (COPY this structure):
|
||||
{"suggestions":[
|
||||
{"type":"bar","title":"Chart Title","data":[{"label":"A","value":100},{"label":"B","value":200}],"description":"...","rationale":"..."},
|
||||
{"type":"line","title":"...","data":[...],"description":"...","rationale":"..."},
|
||||
{"type":"pie","title":"...","data":[...],"description":"...","rationale":"..."}
|
||||
],"analyzedText":"...","detectedData":"...","hasData":true}`,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Analyze this text and suggest 3 appropriate chart types:\n\n${textToAnalyze}`,
|
||||
content: `Extract numbers from this text and suggest 3 charts:\n\n${textToAnalyze}`,
|
||||
},
|
||||
],
|
||||
tools,
|
||||
temperature: 0.3,
|
||||
})
|
||||
|
||||
// 5. Parse AI response
|
||||
// 5. Parse AI response - be very lenient
|
||||
console.log('[suggest-charts] AI response:', text.substring(0, 500))
|
||||
|
||||
let parsed: SuggestChartsResponse
|
||||
try {
|
||||
// Try to extract JSON from the response
|
||||
const jsonMatch = text.match(/\{[\s\S]*\}/)
|
||||
// Clean the response - remove markdown code blocks
|
||||
let cleanText = text
|
||||
.replace(/```json\n?/gi, '')
|
||||
.replace(/```\n?/gi, '')
|
||||
.trim()
|
||||
|
||||
// Find JSON object
|
||||
const jsonMatch = cleanText.match(/\{[\s\S]*\}/)
|
||||
if (!jsonMatch) {
|
||||
throw new Error('No JSON found in response')
|
||||
throw new Error('No JSON found')
|
||||
}
|
||||
parsed = JSON.parse(jsonMatch[0])
|
||||
} catch (e) {
|
||||
console.error('[suggest-charts] Failed to parse AI response:', text)
|
||||
// Return empty suggestions on parse error
|
||||
return Response.json({
|
||||
suggestions: [],
|
||||
analyzedText: textToAnalyze.substring(0, 100),
|
||||
detectedData: 'Failed to parse AI response',
|
||||
hasData: false,
|
||||
} satisfies SuggestChartsResponse)
|
||||
console.error('[suggest-charts] Parse error:', e, 'Raw text:', text)
|
||||
|
||||
// Check if text has ANY numbers - if so, create a simple chart
|
||||
const numbers = textToAnalyze.match(/\d+/g)
|
||||
if (numbers && numbers.length >= 2) {
|
||||
const values = numbers.slice(0, 6).map(n => parseInt(n) || 0)
|
||||
parsed = {
|
||||
suggestions: [
|
||||
{
|
||||
type: 'bar',
|
||||
title: 'Data from Note',
|
||||
data: values.map((v, i) => ({ label: `Item ${i + 1}`, value: v })),
|
||||
description: 'Bar chart of extracted values',
|
||||
rationale: 'Simple comparison of numerical values found'
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
title: 'Data Trend',
|
||||
data: values.map((v, i) => ({ label: `Item ${i + 1}`, value: v })),
|
||||
description: 'Line chart showing progression',
|
||||
rationale: 'Visualizes the sequence of values'
|
||||
},
|
||||
{
|
||||
type: 'pie',
|
||||
title: 'Data Distribution',
|
||||
data: values.map((v, i) => ({ label: `Item ${i + 1}`, value: v })),
|
||||
description: 'Pie chart of value proportions',
|
||||
rationale: 'Shows relative sizes of each value'
|
||||
}
|
||||
],
|
||||
analyzedText: textToAnalyze.substring(0, 100),
|
||||
detectedData: `Found ${numbers.length} numerical values`,
|
||||
hasData: true
|
||||
}
|
||||
} else {
|
||||
return Response.json({
|
||||
suggestions: [],
|
||||
analyzedText: textToAnalyze.substring(0, 100),
|
||||
detectedData: 'No numerical data found',
|
||||
hasData: false,
|
||||
error: 'Add numbers to your note (e.g., "Jan: 100, Feb: 200")',
|
||||
} satisfies SuggestChartsResponse, { status: 200 })
|
||||
}
|
||||
}
|
||||
|
||||
// Validate response structure
|
||||
// Validate and fix response
|
||||
if (!parsed.suggestions || !Array.isArray(parsed.suggestions)) {
|
||||
parsed.suggestions = []
|
||||
}
|
||||
|
||||
// Ensure exactly 3 suggestions when data exists
|
||||
if (parsed.hasData && parsed.suggestions.length !== 3) {
|
||||
parsed.suggestions = parsed.suggestions.slice(0, 3)
|
||||
// Ensure we have 3 suggestions when hasData=true
|
||||
if (parsed.hasData && parsed.suggestions.length < 3) {
|
||||
const baseSuggestion = parsed.suggestions[0]
|
||||
if (baseSuggestion) {
|
||||
const types = ['bar', 'line', 'pie'].filter(t => t !== baseSuggestion.type)
|
||||
while (parsed.suggestions.length < 3 && types.length > 0) {
|
||||
parsed.suggestions.push({ ...baseSuggestion, type: types.shift()! })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each suggestion has required fields
|
||||
parsed.suggestions = parsed.suggestions.filter(s =>
|
||||
s.type && s.title && s.data && Array.isArray(s.data) && s.data.length >= 2
|
||||
)
|
||||
// Ensure each suggestion has valid data
|
||||
parsed.suggestions = parsed.suggestions.filter(s => {
|
||||
if (!s.type || !s.data || !Array.isArray(s.data) || s.data.length < 2) return false
|
||||
// Ensure all data points have label and value
|
||||
s.data = s.data.filter(d => d && typeof d.value === 'number')
|
||||
return s.data.length >= 2
|
||||
})
|
||||
|
||||
// If after filtering we have no valid suggestions, set hasData=false
|
||||
if (parsed.suggestions.length === 0) {
|
||||
parsed.hasData = false
|
||||
}
|
||||
|
||||
// Track usage
|
||||
await trackFeatureUsage(userId, 'suggest_charts', 'suggest-charts', 1)
|
||||
|
||||
@@ -53,6 +53,7 @@ export function ChartSuggestionsDialog({
|
||||
suggestCharts({ content, selection, noteId })
|
||||
.then(data => {
|
||||
if (aborted) return
|
||||
console.log('[ChartSuggestionsDialog] Response:', data)
|
||||
setResponse(data)
|
||||
setLoading(false)
|
||||
})
|
||||
@@ -61,10 +62,10 @@ export function ChartSuggestionsDialog({
|
||||
console.error('[ChartSuggestionsDialog] Error:', err)
|
||||
setResponse({
|
||||
suggestions: [],
|
||||
analyzedText: '',
|
||||
detectedData: '',
|
||||
analyzedText: textToAnalyze?.substring(0, 100) || '',
|
||||
detectedData: 'Error occurred',
|
||||
hasData: false,
|
||||
error: err.message || 'Failed to load suggestions',
|
||||
error: err.message || 'Network error - check console',
|
||||
})
|
||||
setLoading(false)
|
||||
})
|
||||
@@ -192,6 +193,17 @@ export function ChartSuggestionsDialog({
|
||||
<li>• Percentages or proportions</li>
|
||||
<li>• Time-series data</li>
|
||||
</ul>
|
||||
<div className="mt-4 pt-4 border-t border-border/50">
|
||||
<details className="text-left">
|
||||
<summary className="text-xs text-muted-foreground cursor-pointer hover:text-foreground">
|
||||
Debug: Show what was analyzed
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-muted p-2 rounded overflow-auto max-h-32">
|
||||
{textToAnalyze.substring(0, 500)}
|
||||
{textToAnalyze.length > 500 && '...'}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user