diff --git a/memento-note/app/api/ai/suggest-charts/route.ts b/memento-note/app/api/ai/suggest-charts/route.ts new file mode 100644 index 0000000..25fc792 --- /dev/null +++ b/memento-note/app/api/ai/suggest-charts/route.ts @@ -0,0 +1,174 @@ +import { generateText } from 'ai' +import { resolveAiRouteWithTiming } from '@/lib/ai/router' +import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-for-user' +import { getSystemConfig } from '@/lib/config' +import { prisma } from '@/lib/prisma' +import { auth } from '@/auth' +import { toolRegistry } from '@/lib/ai/tools' +import { checkEntitlementOrThrow, QuotaExceededError } from '@/lib/entitlements' +import { trackFeatureUsage } from '@/lib/usage-tracker' + +export const maxDuration = 30 + +interface SuggestChartsRequest { + content: string + selection?: string | null + noteId?: string +} + +interface ChartSuggestion { + type: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'radar' | 'funnel' | 'gauge' + title: string + data: { label: string; value: number }[] + description: string + rationale?: string +} + +interface SuggestChartsResponse { + suggestions: ChartSuggestion[] + analyzedText: string + detectedData: string + hasData: boolean +} + +export async function POST(req: Request) { + // 1. Auth check + const session = await auth() + if (!session?.user?.id) { + return new Response('Unauthorized', { status: 401 }) + } + const userId = session.user.id + + // 1.5 Quota check + try { + const sysConfigEarly = await getSystemConfig() + const { usedByok: willUseByok } = await willUseByokForLane('suggest-charts', sysConfigEarly, userId) + if (!willUseByok) { + await checkEntitlementOrThrow(userId, 'ai') + } + } catch (err) { + if (err instanceof QuotaExceededError) { + return Response.json(err.toJSON(), { status: 402 }) + } + console.error('[suggest-charts] Quota check error (fail-open):', err) + } + + // 2. Parse request body + const body = await req.json() as SuggestChartsRequest + const { content, selection, noteId } = body + + if (!content || content.trim().length === 0) { + return Response.json({ + error: 'Content is required', + suggestions: [], + analyzedText: '', + detectedData: '', + hasData: false, + } satisfies SuggestChartsResponse, { status: 400 }) + } + + const textToAnalyze = selection && selection.trim() ? selection.trim() : content.trim() + + // 3. Build AI context + 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 + const { text } = await generateText({ + model: provider(model), + system: `You are a data visualization assistant. Analyze the provided text and suggest appropriate chart types. + +CRITICAL: Extract ONLY numerical data present in the text. Do NOT invent values. +- 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 +- Provide clear rationale for each chart type choice + +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 +}`, + messages: [ + { + role: 'user', + content: `Analyze this text and suggest 3 appropriate chart types:\n\n${textToAnalyze}`, + }, + ], + tools, + temperature: 0.3, + }) + + // 5. Parse AI response + let parsed: SuggestChartsResponse + try { + // Try to extract JSON from the response + const jsonMatch = text.match(/\{[\s\S]*\}/) + if (!jsonMatch) { + throw new Error('No JSON found in response') + } + 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) + } + + // Validate response structure + 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) + } + + // 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 + ) + + // Track usage + await trackFeatureUsage(userId, 'ai', 'suggest-charts', 1) + + return Response.json(parsed satisfies SuggestChartsResponse) + + } catch (error) { + console.error('[suggest-charts] Error:', error) + return Response.json({ + error: 'Failed to generate chart suggestions', + suggestions: [], + analyzedText: textToAnalyze.substring(0, 100), + detectedData: 'Error occurred during analysis', + hasData: false, + } satisfies SuggestChartsResponse, { status: 500 }) + } +} diff --git a/memento-note/components/chart-suggestions-dialog.tsx b/memento-note/components/chart-suggestions-dialog.tsx new file mode 100644 index 0000000..a44f128 --- /dev/null +++ b/memento-note/components/chart-suggestions-dialog.tsx @@ -0,0 +1,244 @@ +'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 } from 'lucide-react' +import { cn } from '@/lib/utils' + +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(null) + const [loading, setLoading] = useState(false) + const [selectedIndex, setSelectedIndex] = useState(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 + setResponse(data) + setLoading(false) + }) + .catch(err => { + if (aborted) return + console.error('[ChartSuggestionsDialog] Error:', err) + setResponse({ + suggestions: [], + analyzedText: '', + detectedData: '', + hasData: false, + error: err.message || 'Failed to load suggestions', + }) + 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( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+ +
+

Chart Suggestions

+

+ {loading ? ( + + + Analyzing {isAnalyzingSelection ? 'selection' : 'note'} ({wordCount} words)... + + ) : response?.hasData ? ( + `Found ${response?.detectedData || 'data'}` + ) : ( + 'No data detected' + )} +

+
+
+ +
+ + {/* Content */} +
+ {loading ? ( +
+
+ +

Analyzing your content for chart data...

+
+
+ ) : response?.error ? ( +
+
+ +

Error

+

{response.error}

+
+
+ ) : !response?.hasData ? ( +
+
+ +

No Data Detected

+

+ Try adding numerical data to your note. Charts work best with: +

+
    +
  • • Sales figures, metrics, or measurements
  • +
  • • Lists with values (e.g., "Jan: 5000, Feb: 7500")
  • +
  • • Percentages or proportions
  • +
  • • Time-series data
  • +
+
+
+ ) : ( +
+

+ Select a chart type to insert: +

+ + {/* Chart suggestions grid */} +
+ {response.suggestions.map((suggestion, index) => { + const isSelected = selectedIndex === index + const markdown = chartSuggestionToMarkdown(suggestion) + + return ( + + ) + })} +
+ + {/* Keyboard hint */} +
+ ← → Navigate + ↵ Select + Esc Cancel +
+
+ )} +
+
+
, + document.body + ) +} diff --git a/memento-note/components/rich-text-editor.tsx b/memento-note/components/rich-text-editor.tsx index 0945e60..c29080d 100644 --- a/memento-note/components/rich-text-editor.tsx +++ b/memento-note/components/rich-text-editor.tsx @@ -21,6 +21,8 @@ import { TableHeader } from '@tiptap/extension-table-header' import Superscript from '@tiptap/extension-superscript' import Subscript from '@tiptap/extension-subscript' import Typography from '@tiptap/extension-typography' +import { ChartExtension } from './tiptap-chart-extension' +import { ChartSuggestionsDialog } from './chart-suggestions-dialog' import type { Editor } from '@tiptap/core' import type { EditorState } from '@tiptap/pm/state' import { @@ -30,7 +32,7 @@ import { Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink, FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight, Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus, - SpellCheck, Languages, BookOpen, Presentation + SpellCheck, Languages, BookOpen, Presentation, BarChart3 } from 'lucide-react' import { cn } from '@/lib/utils' import { toast } from 'sonner' @@ -140,6 +142,11 @@ const slashCommands: SlashItem[] = [ window.dispatchEvent(event) } }, + { + title: 'Suggest Charts', description: 'AI suggère des graphiques basés sur votre contenu', icon: BarChart3, category: 'IA Note', isAi: true, command: (e) => { + // Handler will be called by SlashCommandMenu + } + }, ] async function aiReformulate(text: string, option: string, language?: string): Promise { @@ -218,6 +225,7 @@ export const RichTextEditor = forwardRef(content || '') + // Chart suggestions dialog state + const [chartSuggestionsOpen, setChartSuggestionsOpen] = useState(false) + const [currentNoteContent, setCurrentNoteContent] = useState(content || '') + useEffect(() => { if (editor && content !== undefined && content !== lastEmittedContent.current) { editor.commands.setContent(content || '') lastEmittedContent.current = content || '' } + // Update current note content for chart suggestions + if (content !== undefined) { + setCurrentNoteContent(content || '') + } }, [content, editor]) useImperativeHandle(ref, () => ({ getEditor: () => editor }), [editor]) + // Chart suggestion handlers + const handleOpenChartSuggestions = useCallback(() => { + if (!editor || !editor.isEditable) return + + // Get current selection text if any + const { from, to, empty } = editor.state.selection + const selectionText = !empty ? editor.state.doc.textBetween(from, to, ' ') : null + + setChartSuggestionsOpen(true) + }, [editor]) + + const handleSelectChart = useCallback((markdown: string) => { + if (!editor || !editor.isEditable) return + + try { + // Insert the chart markdown at current position + const { from } = editor.state.selection + + // Check if we're in the middle of text, if so create a new paragraph + const $pos = editor.state.doc.resolve(from) + const isInTextNode = $pos.parent.type.name === 'paragraph' + + if (isInTextNode && $pos.parentOffset > 0) { + // Create a new paragraph after current one + const afterPos = $pos.after() + // Ensure we don't exceed document bounds + if (afterPos < editor.state.doc.content.size) { + editor.chain().focus().selectParentNode().insertContentAt(afterPos, '

').run() + } + } + + // Insert the chart + editor.chain().focus().insertContent(markdown).run() + } catch (error) { + console.error('[handleSelectChart] Failed to insert chart:', error) + toast.error('Failed to insert chart. Please try again.') + } + }, [editor]) + return (
{editor && ( @@ -287,13 +342,23 @@ export const RichTextEditor = forwardRef )} - {editor && } + {editor && } {imageInsert.open && ( )} + + {chartSuggestionsOpen && ( + setChartSuggestionsOpen(false)} + onSelectChart={handleSelectChart} + /> + )}
) } @@ -539,7 +604,7 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) { ) } -function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertImage: (editor: Editor) => void }) { +function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor: Editor; onInsertImage: (editor: Editor) => void; onSuggestCharts: () => void }) { const { t } = useLanguage() const [isOpen, setIsOpen] = useState(false) const [query, setQuery] = useState('') @@ -580,6 +645,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI { ...slashCommands[25], title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), categoryId: 'formatting' }, { ...slashCommands[26], title: t('richTextEditor.slashDiagram'), description: t('richTextEditor.slashDiagramDesc'), categoryId: 'ai' }, { ...slashCommands[27], title: t('richTextEditor.slashSlides'), description: t('richTextEditor.slashSlidesDesc'), categoryId: 'ai' }, + { ...slashCommands[28], title: 'Suggest Charts', description: 'AI suggère des graphiques basés sur votre contenu', categoryId: 'ai' }, ] const closeMenu = useCallback(() => { @@ -615,10 +681,12 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI toastAi(err) } finally { setAiLoading(false) } + } else if (item.title === 'Suggest Charts') { + deleteSlashText(); closeMenu(); onSuggestCharts() } else { deleteSlashText(); item.command(editor); closeMenu() } - }, [editor, closeMenu, deleteSlashText, onInsertImage, t]) + }, [editor, closeMenu, deleteSlashText, onInsertImage, onSuggestCharts, t]) const presentCategoryIds = new Set(localCommands.map(c => c.categoryId)) const allCategories = ORDERED_SLASH_CATEGORIES.filter(id => presentCategoryIds.has(id)) diff --git a/memento-note/components/tiptap-chart-extension.tsx b/memento-note/components/tiptap-chart-extension.tsx new file mode 100644 index 0000000..4f9ec86 --- /dev/null +++ b/memento-note/components/tiptap-chart-extension.tsx @@ -0,0 +1,171 @@ +'use client' + +import { Node } from '@tiptap/core' +import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react' +import { NoteChartFromCode } from './note-chart' +import { useState } from 'react' +import { Code, BarChart3, AlertCircle } from 'lucide-react' +import { cn } from '@/lib/utils' + +/** + * ChartExtension - TipTap Node extension for rendering chart blocks + * + * Detects
 blocks and renders them
+ * as visual charts using the NoteChartFromCode component.
+ */
+export const ChartExtension = Node.create({
+  name: 'chartBlock',
+
+  group: 'block',
+
+  code: true,
+
+  defining: true,
+
+  addOptions() {
+    return {
+      HTMLAttributes: {},
+    }
+  },
+
+  addAttributes() {
+    return {
+      code: {
+        default: '',
+        parseHTML: element => {
+          if (element instanceof HTMLElement) {
+            const codeEl = element.querySelector('code')
+            return codeEl?.textContent || ''
+          }
+          return ''
+        },
+        renderHTML: () => ({})
+      },
+      language: {
+        default: 'chart',
+        parseHTML: element => {
+          if (element instanceof HTMLElement) {
+            const codeEl = element.querySelector('code')
+            // Check for class="language-chart" or data-language="chart"
+            if (codeEl?.classList.contains('language-chart')) return 'chart'
+            return element.getAttribute('data-language') || 'chart'
+          }
+          return 'chart'
+        },
+        renderHTML: attributes => ({
+          'data-language': attributes.language,
+        })
+      }
+    }
+  },
+
+  parseHTML() {
+    return [
+      {
+        tag: 'pre',
+        getAttrs: node => {
+          if (typeof node === 'string') return false
+          const element = node as HTMLElement
+          const codeEl = element.querySelector('code')
+
+          // Detect chart blocks by class="language-chart"
+          if (codeEl && codeEl.classList.contains('language-chart')) {
+            return {}
+          }
+
+          return false
+        }
+      }
+    ]
+  },
+
+  renderHTML({ HTMLAttributes }) {
+    return ['pre', { ...HTMLAttributes, class: 'language-chart' }, ['code', { class: 'language-chart' }, 0]]
+  },
+
+  addNodeView() {
+    return ReactNodeViewRenderer(ChartBlockView, {
+      contentEditable: false,
+    })
+  }
+})
+
+/**
+ * ChartBlockView - React component for rendering chart blocks in TipTap
+ *
+ * Features:
+ * - Visual chart rendering with NoteChartFromCode
+ * - Toggle between visual and code view
+ * - Edit mode for modifying chart data
+ * - Error handling for invalid chart data
+ */
+function ChartBlockView(props: any) {
+  const [isEditing, setIsEditing] = useState(false)
+  const [parseError, setParseError] = useState(false)
+
+  const code = props.node?.attrs?.code || ''
+
+  // Check if chart code is valid when not editing
+  const isValidChart = !isEditing && code.trim().length > 0
+
+  if (isEditing) {
+    return (
+      
+        
+ + Chart Code + +
+ +
+ ) + } + + return ( + +
+ {/* Chart visual rendering */} + {isValidChart ? ( + + ) : ( +
+
+ +
+

Invalid Chart

+

This chart block contains invalid or empty data.

+
+
+
+ )} + + {/* Edit button - visible on hover */} + + + {/* Chart type indicator */} + {isValidChart && ( +
+
+ + Chart +
+
+ )} +
+
+ ) +} diff --git a/memento-note/lib/ai/services/chart-suggestion.service.ts b/memento-note/lib/ai/services/chart-suggestion.service.ts new file mode 100644 index 0000000..87acc12 --- /dev/null +++ b/memento-note/lib/ai/services/chart-suggestion.service.ts @@ -0,0 +1,80 @@ +/** + * Chart Suggestion Service + * Frontend service for calling the AI chart suggestions API + */ + +export interface ChartSuggestion { + type: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'radar' | 'funnel' | 'gauge' + title: string + data: { label: string; value: number }[] + description: string + rationale?: string +} + +export interface SuggestChartsResponse { + suggestions: ChartSuggestion[] + analyzedText: string + detectedData: string + hasData: boolean + error?: string +} + +export interface SuggestChartsRequest { + content: string + selection?: string | null + noteId?: string +} + +/** + * Call the AI chart suggestions API + * @param request - The request parameters + * @returns Promise with the chart suggestions + */ +export async function suggestCharts(request: SuggestChartsRequest): Promise { + try { + const response = await fetch('/api/ai/suggest-charts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) + return { + suggestions: [], + analyzedText: '', + detectedData: '', + hasData: false, + error: errorData.error || `HTTP ${response.status}`, + } + } + + const data = await response.json() + return data as SuggestChartsResponse + } catch (error) { + console.error('[suggestCharts] Network error:', error) + return { + suggestions: [], + analyzedText: '', + detectedData: '', + hasData: false, + error: error instanceof Error ? error.message : 'Network error', + } + } +} + +/** + * Convert a chart suggestion to markdown code block format + * @param suggestion - The chart suggestion to convert + * @returns Markdown code block string + */ +export function chartSuggestionToMarkdown(suggestion: ChartSuggestion): string { + const lines = [ + suggestion.type, + suggestion.title, + ...suggestion.data.map(d => `${d.label}: ${d.value}`), + ] + return `\`\`\`chart\n${lines.join('\n')}\n\`\`\`` +} diff --git a/memento-note/lib/ai/tools/chart-suggestion.tool.ts b/memento-note/lib/ai/tools/chart-suggestion.tool.ts new file mode 100644 index 0000000..1c783cb --- /dev/null +++ b/memento-note/lib/ai/tools/chart-suggestion.tool.ts @@ -0,0 +1,104 @@ +/** + * Chart Suggestion Tool for Notes + * AI analyzes note content and suggests appropriate chart types with data + */ + +import { tool } from 'ai' +import { z } from 'zod' +import { toolRegistry } from './registry' + +// Chart suggestion data structures +export interface ChartSuggestion { + type: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'radar' | 'funnel' | 'gauge' + title: string + data: { label: string; value: number }[] + description: string + rationale?: string +} + +export interface SuggestChartsResponse { + suggestions: ChartSuggestion[] + analyzedText: string + detectedData: string + hasData: boolean +} + +toolRegistry.register({ + name: 'suggest_charts', + description: 'Analyze content and suggest appropriate chart types with extracted data', + isInternal: true, + buildTool: (ctx) => + tool({ + description: `Analyze the provided text content and suggest 3 appropriate chart types with extracted data. + +Available chart types: +- "bar": Vertical bar chart (best for comparing values across categories) +- "horizontal-bar": Horizontal bar chart (best for long category labels) +- "line": Line chart (best for trends over time or sequences) +- "area": Area chart (filled line chart, best for showing magnitude over time) +- "pie": Pie chart (best for showing proportions/percentages of a whole) +- "radar": Radar chart (best for comparing multiple dimensions) +- "funnel": Funnel chart (best for showing stages in a process) +- "gauge": Gauge chart (best for single values vs a target) + +CRITICAL RULES: +1. Extract ONLY numerical data present in the text - do NOT invent or fabricate values +2. If fewer than 2 data points exist, return empty suggestions array with hasData=false +3. Each suggestion MUST use the SAME extracted data - only the chart type differs +4. Provide a clear rationale explaining WHY each chart type suits the data +5. Generate meaningful labels - if the text provides context (months, categories, names), use those; otherwise use generic labels like "Item 1", "Item 2", etc. + +Data extraction examples: +- "Sales: Jan $5000, Feb $7500, Mar $6200" → [{label:"Jan",value:5000}, {label:"Feb",value:7500}, {label:"Mar",value:6200}] +- "Product A: 45%, Product B: 30%, Product C: 25%" → [{label:"Product A",value:45}, {label:"Product B",value:30}, {label:"Product C",value:25}] +- "Progress: Q1=10, Q2=25, Q3=40, Q4=60" → [{label:"Q1",value:10}, {label:"Q2",value:25}, {label:"Q3",value:40}, {label:"Q4",value:60}] + +Output format: +Return exactly 3 chart suggestions with different types. Order by relevance (most suitable first). + +Example response for sales data: +{ + "suggestions": [ + { + "type": "bar", + "title": "Sales by Month", + "data": [{"label":"Jan","value":5000},{"label":"Feb","value":7500},{"label":"Mar","value":6200}], + "description": "Bar chart comparing sales across months", + "rationale": "Best for direct comparison of values between categories" + }, + { + "type": "line", + "title": "Sales Trend", + "data": [{"label":"Jan","value":5000},{"label":"Feb","value":7500},{"label":"Mar","value":6200}], + "description": "Line chart showing sales progression over time", + "rationale": "Ideal for visualizing trends and changes over time periods" + }, + { + "type": "area", + "title": "Sales Volume", + "data": [{"label":"Jan","value":5000},{"label":"Feb","value":7500},{"label":"Mar","value":6200}], + "description": "Area chart emphasizing sales magnitude", + "rationale": "Similar to line but emphasizes volume/proportion visually" + } + ], + "analyzedText": "Sales: Jan $5000, Feb $7500, Mar $6200", + "detectedData": "3 data points: sales figures for Jan, Feb, Mar", + "hasData": true +}`, + inputSchema: z.object({ + content: z.string().describe('The full note content to analyze for chart data'), + selection: z.string().optional().describe('Optional selected text - if provided, analyze only this instead of full content'), + }), + execute: async ({ content, selection }) => { + const textToAnalyze = selection && selection.trim() ? selection.trim() : content.trim() + + // This will be processed by the AI model + // The AI will extract data and generate suggestions + return { + textToAnalyze, + // The actual suggestion generation happens in the AI response + // This tool provides the context for the AI to work with + } + }, + }), +})