+ )
+}
+
+// Parse chart from JSON or simple format
+export function parseChartFromCode(code: string): NoteChartProps | null {
+ try {
+ // Try JSON first
+ const parsed = JSON.parse(code)
+ if (parsed.type && parsed.data && Array.isArray(parsed.data)) {
+ return {
+ type: parsed.type,
+ title: parsed.title,
+ data: parsed.data,
+ colors: parsed.colors,
+ showLegend: parsed.showLegend,
+ height: parsed.height,
+ }
+ }
+ } catch {}
+
+ // Try simple format: type, title, then label: value lines
+ const lines = code.trim().split('\n').map(l => l.trim()).filter(Boolean)
+ if (lines.length === 0) return null
+
+ const firstLine = lines[0].toLowerCase()
+ let type: NoteChartProps['type'] = 'bar'
+ let title = ''
+
+ if (['bar', 'horizontal-bar', 'line', 'area', 'pie', 'radar', 'funnel', 'gauge'].includes(firstLine)) {
+ type = firstLine as NoteChartProps['type']
+ } else if (lines[0].includes(':')) {
+ // No type specified, default to bar
+ } else {
+ title = lines[0]
+ }
+
+ const data: ChartData[] = []
+ for (const line of lines.slice(title ? 1 : type !== lines[0]?.toLowerCase() ? 0 : 1)) {
+ const parts = line.split(/[:|]/)
+ if (parts.length >= 2) {
+ const label = parts[0].trim()
+ const value = parseFloat(parts[1].trim())
+ if (!isNaN(value)) {
+ data.push({ label, value })
+ }
+ }
+ }
+
+ if (data.length === 0) return null
+
+ return { type, title: title || undefined, data }
+}
+
+export function NoteChartFromCode({ code }: { code: string }) {
+ const props = useMemo(() => parseChartFromCode(code), [code])
+ if (!props) return
Format de graphique invalide
+ return
+}
diff --git a/memento-note/lib/ai/tools/chart.tool.ts b/memento-note/lib/ai/tools/chart.tool.ts
new file mode 100644
index 0000000..7ddce1e
--- /dev/null
+++ b/memento-note/lib/ai/tools/chart.tool.ts
@@ -0,0 +1,117 @@
+/**
+ * Chart Tool for Notes
+ * Allows AI to generate inline charts from note data
+ */
+
+import { tool } from 'ai'
+import { z } from 'zod'
+import { toolRegistry } from './registry'
+
+// Chart generation tool - inserts chart as markdown code block
+toolRegistry.register({
+ name: 'generate_chart',
+ description: 'Generate an inline chart from data and insert it into a note as markdown. The chart will be rendered directly in the note.',
+ isInternal: true,
+ buildTool: (ctx) =>
+ tool({
+ description: `Generate an inline chart from data and return it as a markdown code block that can be inserted into a note.
+
+Available chart types:
+- "bar": Vertical bar chart
+- "horizontal-bar": Horizontal bar chart
+- "line": Line chart for trends over time
+- "area": Area chart (filled line)
+- "pie": Pie/donut chart for proportions
+- "radar": Radar chart for comparing multiple dimensions
+- "funnel": Funnel chart for stages/conversions
+- "gauge": Gauge/meter for a single value
+
+Data format: Array of objects with "label" (string) and "value" (number).
+
+IMPORTANT:
+- Extract data from the note content when possible
+- Keep labels short (max 20 characters)
+- Round values to reasonable precision (max 2 decimal places)
+- For time series, use short date formats (Jan, Feb, Mar or 2023, 2024)
+- Maximum 12 data points for readability
+- The output will be rendered as a visual chart in the note`,
+ inputSchema: z.object({
+ chartType: z.enum(['bar', 'horizontal-bar', 'line', 'area', 'pie', 'radar', 'funnel', 'gauge']).describe('Type of chart to generate'),
+ title: z.string().optional().describe('Optional title for the chart'),
+ data: z.array(z.object({
+ label: z.string().describe('Label for the data point (e.g., month name, category)'),
+ value: z.number().describe('Numeric value for the data point'),
+ })).describe('Array of data points with label and value'),
+ insertLocation: z.enum(['append', 'prepend', 'replace']).default('append').describe('Where to insert the chart in the note'),
+ targetNoteId: z.string().optional().describe('Optional: specific note ID to update. If not provided, use the current note.'),
+ }),
+ execute: async ({ chartType, title, data, insertLocation, targetNoteId }) => {
+ try {
+ // Generate the markdown code block for the chart
+ const chartData = data.map(d => `${d.label}: ${d.value}`).join('\n')
+ const chartMarkdown = `\`\`\`chart
+${chartType}${title ? `\n${title}` : ''}
+${chartData}
+\`\`\`\n`
+
+ return {
+ success: true,
+ chartMarkdown,
+ chartType,
+ dataPointCount: data.length,
+ message: `Chart generated with ${data.length} data points. Insert this markdown into the note.`,
+ }
+ } catch (e: any) {
+ return { success: false, error: `Chart generation failed: ${e.message}` }
+ }
+ },
+ }),
+})
+
+// Quick chart insert tool - directly updates a note with a chart
+toolRegistry.register({
+ name: 'insert_chart_in_note',
+ description: 'Insert a chart directly into a note. Reads the note, extracts relevant data, generates a chart, and updates the note content.',
+ isInternal: true,
+ buildTool: (ctx) =>
+ tool({
+ description: `Insert a chart directly into a note. This tool will:
+1. Read the current note content
+2. Extract relevant data from the note (sales, metrics, comparisons, etc.)
+3. Generate an appropriate chart type based on the data
+4. Insert the chart markdown into the note
+
+Use this when the user says "make a chart", "create a graph", "visualize this data", etc.
+
+Choose the chart type based on the data:
+- Use "bar" for comparing values across categories (default)
+- Use "horizontal-bar" when labels are long
+- Use "line" or "area" for time series or trends
+- Use "pie" for showing proportions/percentages
+- Use "radar" for comparing multiple attributes
+- Use "funnel" for stages or conversion data
+- Use "gauge" for progress/KPI (single value)`,
+ inputSchema: z.object({
+ noteId: z.string().describe('The note ID to read and update'),
+ chartHint: z.string().optional().describe('Optional hint about what to chart (e.g., "sales by month", "comparison of products")'),
+ insertLocation: z.enum(['append', 'prepend', 'before-section', 'after-section']).default('append').describe('Where to insert the chart'),
+ sectionMarker: z.string().optional().describe('For before-section/after-section: the heading text to find (e.g., "## Sales Data")'),
+ }),
+ execute: async ({ noteId, chartHint, insertLocation, sectionMarker }) => {
+ try {
+ // We'll return the instructions for the AI to format the chart
+ // The actual note update will be done by note_find_and_update or note_update
+ return {
+ success: true,
+ instructions: 'Generate a chart markdown block using the generate_chart tool, then use note_update or note_find_and_update to insert it into the note.',
+ noteId,
+ chartHint,
+ insertLocation,
+ sectionMarker,
+ }
+ } catch (e: any) {
+ return { success: false, error: `Failed to prepare chart insertion: ${e.message}` }
+ }
+ },
+ }),
+})
diff --git a/memento-note/lib/ai/tools/index.ts b/memento-note/lib/ai/tools/index.ts
index cc589f4..b153fe3 100644
--- a/memento-note/lib/ai/tools/index.ts
+++ b/memento-note/lib/ai/tools/index.ts
@@ -15,6 +15,7 @@ import './pptx.tool'
import './slides.tool'
import './document-search.tool'
import './task-extract.tool'
+import './chart.tool'
// Re-export registry
export { toolRegistry, type ToolContext, type RegisteredTool } from './registry'
diff --git a/memento-note/lib/ai/tools/registry.ts b/memento-note/lib/ai/tools/registry.ts
index e11883c..c4007d9 100644
--- a/memento-note/lib/ai/tools/registry.ts
+++ b/memento-note/lib/ai/tools/registry.ts
@@ -52,7 +52,7 @@ class ToolRegistry {
* When webOnly is true, only web tools are included (no note access).
*/
buildToolsForChat(ctx: ToolContext & { webOnly?: boolean }): Record {
- const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read', 'note_find_and_update', 'document_search', 'task_extract']
+ const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read', 'note_find_and_update', 'document_search', 'task_extract', 'generate_chart', 'insert_chart_in_note']
// Add web tools only when user toggled web search AND config is present
if (ctx.webSearch) {
diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json
index 12d7977..447a29e 100644
--- a/memento-note/locales/en.json
+++ b/memento-note/locales/en.json
@@ -635,6 +635,27 @@
"generationTools": "Generation tools",
"generateSlidesLoading": "⏳ Generating presentation...",
"generateDiagramLoading": "⏳ Generating diagram...",
+ "generateChartLoading": "⏳ Generating chart...",
+ "chart": {
+ "title": "Chart",
+ "generate": "Generate chart",
+ "generateDesc": "Create a visualization from data",
+ "type": "Chart type",
+ "typeBar": "Bar",
+ "typeBarH": "Horizontal Bar",
+ "typeLine": "Line",
+ "typeArea": "Area",
+ "typePie": "Pie",
+ "typeRadar": "Radar",
+ "typeFunnel": "Funnel",
+ "typeGauge": "Gauge",
+ "autoDetect": "Auto detect",
+ "insertInNote": "Insert in note",
+ "chartReady": "Chart ready!",
+ "insertedMessage": "Chart inserted in note",
+ "noDataError": "No data found to create chart",
+ "invalidFormat": "Invalid data format"
+ },
"errorShort": "Error",
"readyToast": "Ready!",
"downloadFailedToast": "Download failed",
diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json
index 9cc0b2f..0e0f6d6 100644
--- a/memento-note/locales/fr.json
+++ b/memento-note/locales/fr.json
@@ -641,6 +641,27 @@
"generationTools": "Outils de génération",
"generateSlidesLoading": "⏳ Génération de la présentation...",
"generateDiagramLoading": "⏳ Génération du diagramme...",
+ "generateChartLoading": "⏳ Génération du graphique...",
+ "chart": {
+ "title": "Graphique",
+ "generate": "Générer un graphique",
+ "generateDesc": "Créer une visualisation à partir des données",
+ "type": "Type de graphique",
+ "typeBar": "Barres",
+ "typeBarH": "Barres horizontales",
+ "typeLine": "Ligne",
+ "typeArea": "Aire",
+ "typePie": "Circulaire",
+ "typeRadar": "Radar",
+ "typeFunnel": "Entonnoir",
+ "typeGauge": "Jauge",
+ "autoDetect": "Détection auto",
+ "insertInNote": "Insérer dans la note",
+ "chartReady": "Graphique prêt !",
+ "insertedMessage": "Graphique inséré dans la note",
+ "noDataError": "Aucune donnée trouvée pour créer un graphique",
+ "invalidFormat": "Format de données invalide"
+ },
"errorShort": "Erreur",
"readyToast": "Prêt !",
"downloadFailedToast": "Échec du téléchargement",