feat(ai): add AI chart suggestions in TipTap editor
Implement slash command "/suggest-charts" that analyzes note content and suggests 3 appropriate chart types with visual previews. Created: - chart-suggestion.tool.ts: AI tool for data extraction and chart recommendations - suggest-charts/route.ts: API endpoint for chart suggestions - chart-suggestion.service.ts: Frontend service layer - chart-suggestions-dialog.tsx: Modal with 3 chart proposals and thumbnails - tiptap-chart-extension.tsx: TipTap Node extension for rendering chart blocks Modified: - rich-text-editor.tsx: Added slash command, toolbar button, and dialog integration Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
171
memento-note/components/tiptap-chart-extension.tsx
Normal file
171
memento-note/components/tiptap-chart-extension.tsx
Normal file
@@ -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 <pre><code class="language-chart"> 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 (
|
||||
<NodeViewWrapper className="notion-chart-code-block my-4">
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-t-lg border-b border-border">
|
||||
<Code className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Chart Code</span>
|
||||
<button
|
||||
onClick={() => setIsEditing(false)}
|
||||
className="ml-auto text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
<NodeViewContent as="pre" className="p-4 bg-muted/30 rounded-b-lg overflow-x-auto" />
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="notion-chart-block my-4 group relative">
|
||||
<div className="relative">
|
||||
{/* Chart visual rendering */}
|
||||
{isValidChart ? (
|
||||
<NoteChartFromCode code={code} />
|
||||
) : (
|
||||
<div className="my-6 rounded-xl border border-dashed border-border bg-muted/30 p-6">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<div>
|
||||
<p className="font-medium">Invalid Chart</p>
|
||||
<p className="text-sm">This chart block contains invalid or empty data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit button - visible on hover */}
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className={cn(
|
||||
'absolute top-2 right-2 p-2 rounded-lg bg-background/80 backdrop-blur border border-border shadow-sm opacity-0 group-hover:opacity-100 transition-opacity',
|
||||
'hover:bg-background hover:shadow-md'
|
||||
)}
|
||||
title="Edit chart source"
|
||||
>
|
||||
<Code className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Chart type indicator */}
|
||||
{isValidChart && (
|
||||
<div className="absolute top-2 left-2 px-2 py-1 rounded bg-background/80 backdrop-blur border border-border shadow-sm opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
<span>Chart</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user