- 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>
288 lines
11 KiB
TypeScript
288 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { NoteChartFromCode } from './note-chart'
|
|
import { suggestCharts, chartSuggestionToMarkdown, type ChartSuggestion, type SuggestChartsResponse } from '@/lib/ai/services/chart-suggestion.service'
|
|
import { BarChart3, X, Search, AlertCircle, Sparkles } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
// Chart type to color mapping for visual variety
|
|
const CHART_TYPE_COLORS: Record<string, string> = {
|
|
bar: 'bg-blue-500/10 text-blue-600 border-blue-500/20',
|
|
'horizontal-bar': 'bg-indigo-500/10 text-indigo-600 border-indigo-500/20',
|
|
line: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20',
|
|
area: 'bg-teal-500/10 text-teal-600 border-teal-500/20',
|
|
pie: 'bg-violet-500/10 text-violet-600 border-violet-500/20',
|
|
radar: 'bg-fuchsia-500/10 text-fuchsia-600 border-fuchsia-500/20',
|
|
funnel: 'bg-amber-500/10 text-amber-600 border-amber-500/20',
|
|
gauge: 'bg-rose-500/10 text-rose-600 border-rose-500/20',
|
|
}
|
|
|
|
interface ChartSuggestionsDialogProps {
|
|
isOpen: boolean
|
|
content: string
|
|
selection: string | null
|
|
noteId?: string
|
|
onClose: () => void
|
|
onSelectChart: (markdown: string) => void
|
|
}
|
|
|
|
export function ChartSuggestionsDialog({
|
|
isOpen,
|
|
content,
|
|
selection,
|
|
noteId,
|
|
onClose,
|
|
onSelectChart,
|
|
}: ChartSuggestionsDialogProps) {
|
|
const [response, setResponse] = useState<SuggestChartsResponse | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
|
|
|
// Reset state when dialog opens
|
|
useEffect(() => {
|
|
let aborted = false
|
|
|
|
if (isOpen) {
|
|
setResponse(null)
|
|
setLoading(true)
|
|
setSelectedIndex(null)
|
|
|
|
// Call the suggestion API
|
|
suggestCharts({ content, selection, noteId })
|
|
.then(data => {
|
|
if (aborted) return
|
|
console.log('[ChartSuggestionsDialog] Response:', data)
|
|
setResponse(data)
|
|
setLoading(false)
|
|
})
|
|
.catch(err => {
|
|
if (aborted) return
|
|
console.error('[ChartSuggestionsDialog] Error:', err)
|
|
setResponse({
|
|
suggestions: [],
|
|
analyzedText: textToAnalyze?.substring(0, 100) || '',
|
|
detectedData: 'Error occurred',
|
|
hasData: false,
|
|
error: err.message || 'Network error - check console',
|
|
})
|
|
setLoading(false)
|
|
})
|
|
}
|
|
|
|
return () => {
|
|
aborted = true
|
|
}
|
|
}, [isOpen, content, selection, noteId])
|
|
|
|
// Handle keyboard shortcuts
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
onClose()
|
|
}
|
|
if (e.key === 'ArrowRight' && response?.suggestions) {
|
|
setSelectedIndex(i => i === null ? 0 : (i + 1) % response.suggestions.length)
|
|
}
|
|
if (e.key === 'ArrowLeft' && response?.suggestions) {
|
|
setSelectedIndex(i => i === null ? 0 : (i === 0 ? response.suggestions.length - 1 : i - 1))
|
|
}
|
|
if (e.key === 'Enter' && selectedIndex !== null && response?.suggestions?.[selectedIndex]) {
|
|
onSelectChart(chartSuggestionToMarkdown(response.suggestions[selectedIndex]))
|
|
onClose()
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
}, [isOpen, onClose, selectedIndex, response, onSelectChart])
|
|
|
|
if (!isOpen) return null
|
|
|
|
const textToAnalyze = selection || content
|
|
const isAnalyzingSelection = !!selection
|
|
const wordCount = textToAnalyze.split(/\s+/).length
|
|
|
|
const handleSelectChart = (suggestion: ChartSuggestion) => {
|
|
onSelectChart(chartSuggestionToMarkdown(suggestion))
|
|
onClose()
|
|
}
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={onClose}>
|
|
<div
|
|
className="bg-background rounded-xl shadow-lg w-full max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b">
|
|
<div className="flex items-center gap-3">
|
|
<BarChart3 className="w-5 h-5 text-blue-500" />
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Chart Suggestions</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
{loading ? (
|
|
<span className="flex items-center gap-2">
|
|
<Search className="w-3 h-3 animate-spin" />
|
|
Analyzing {isAnalyzingSelection ? 'selection' : 'note'} ({wordCount} words)...
|
|
</span>
|
|
) : response?.hasData ? (
|
|
`Found ${response?.detectedData || 'data'}`
|
|
) : (
|
|
'No data detected'
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
|
aria-label="Close"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-auto p-6">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<Search className="w-12 h-12 mx-auto mb-4 text-muted-foreground animate-pulse" />
|
|
<p className="text-muted-foreground">Analyzing your content for chart data...</p>
|
|
</div>
|
|
</div>
|
|
) : response?.quotaExceeded ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center max-w-md">
|
|
<Sparkles className="w-12 h-12 mx-auto mb-4 text-orange-500" />
|
|
<h3 className="text-lg font-semibold mb-2">AI Quota Exceeded</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
{response.error || 'You have reached your AI usage limit.'}
|
|
</p>
|
|
<button
|
|
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
|
onClick={() => (window.location.href = '/settings/billing')}
|
|
>
|
|
Upgrade Plan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : response?.error ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center max-w-md">
|
|
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-destructive" />
|
|
<h3 className="text-lg font-semibold mb-2">Error</h3>
|
|
<p className="text-muted-foreground">{response.error}</p>
|
|
</div>
|
|
</div>
|
|
) : !response?.hasData ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center max-w-md">
|
|
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
|
<h3 className="text-lg font-semibold mb-2">No Data Detected</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Try adding numerical data to your note. Charts work best with:
|
|
</p>
|
|
<ul className="text-sm text-muted-foreground text-left inline-block">
|
|
<li>• Sales figures, metrics, or measurements</li>
|
|
<li>• Lists with values (e.g., "Jan: 5000, Feb: 7500")</li>
|
|
<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>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
Select a chart type to insert:
|
|
</p>
|
|
|
|
{/* Chart suggestions grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{response.suggestions.map((suggestion, index) => {
|
|
const isSelected = selectedIndex === index
|
|
const markdown = chartSuggestionToMarkdown(suggestion)
|
|
|
|
return (
|
|
<button
|
|
key={index}
|
|
onClick={() => handleSelectChart(suggestion)}
|
|
onMouseEnter={() => setSelectedIndex(index)}
|
|
className={cn(
|
|
'text-left p-4 rounded-xl border-2 transition-all hover:shadow-md',
|
|
isSelected
|
|
? (CHART_TYPE_COLORS[suggestion.type]?.replace('/10', '/20') || 'border-blue-500 bg-blue-500/5')
|
|
: 'border-border hover:border-blue-500/30'
|
|
)}
|
|
>
|
|
{/* Mini thumbnail */}
|
|
<div className="aspect-video bg-card rounded-lg mb-3 overflow-hidden flex items-center justify-center p-2">
|
|
<div className="w-full scale-75 origin-center">
|
|
<NoteChartFromCode code={markdown} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Chart type badge */}
|
|
<div className="mb-2">
|
|
<span className={cn(
|
|
'text-xs font-medium px-2 py-1 rounded-full border',
|
|
CHART_TYPE_COLORS[suggestion.type] || CHART_TYPE_COLORS.bar
|
|
)}>
|
|
{suggestion.type}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h3 className="font-semibold text-sm mb-1">{suggestion.title}</h3>
|
|
|
|
{/* Description */}
|
|
<p className="text-xs text-muted-foreground mb-2">{suggestion.description}</p>
|
|
|
|
{/* Rationale */}
|
|
{suggestion.rationale && (
|
|
<p className="text-xs text-muted-foreground/70 italic">
|
|
"{suggestion.rationale}"
|
|
</p>
|
|
)}
|
|
|
|
{/* Data preview */}
|
|
<div className="mt-2 pt-2 border-t border-border/50">
|
|
<p className="text-xs text-muted-foreground">
|
|
{suggestion.data.length} data points
|
|
</p>
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Keyboard hint */}
|
|
<div className="flex items-center justify-center gap-4 pt-4 text-xs text-muted-foreground">
|
|
<span>← → Navigate</span>
|
|
<span>↵ Select</span>
|
|
<span>Esc Cancel</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
)
|
|
}
|