'use client' import { Node, mergeAttributes } from '@tiptap/core' import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react' import { Trash2, X, FunctionSquare, Sparkles, Loader2 } from 'lucide-react' import { useState, useEffect, useRef } from 'react' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' import katex from 'katex' function renderKatex(latex: string, display: boolean): { html: string } { try { const html = katex.renderToString(latex, { displayMode: display, throwOnError: false, errorColor: '#ef4444', }) return { html } } catch { return { html: '' } } } const MATH_BUTTONS: Array<{ label: string; insert: string; title: string }> = [ { label: '½', insert: '\\frac{}{}', title: 'Fraction' }, { label: '√', insert: '\\sqrt{}', title: 'Racine' }, { label: '∑', insert: '\\sum_{}^{}', title: 'Somme' }, { label: '∫', insert: '\\int_{}^{}', title: 'Intégrale' }, { label: 'x²', insert: '^{}', title: 'Puissance' }, { label: 'x₂', insert: '_{}', title: 'Indice' }, { label: '×', insert: '\\times', title: 'Multiplication' }, { label: '÷', insert: '\\div', title: 'Division' }, { label: '±', insert: '\\pm', title: 'Plus ou moins' }, { label: '≤', insert: '\\leq', title: 'Inférieur ou égal' }, { label: '≥', insert: '\\geq', title: 'Supérieur ou égal' }, { label: '≠', insert: '\\neq', title: 'Différent' }, { label: '→', insert: '\\rightarrow', title: 'Flèche droite' }, { label: '∞', insert: '\\infty', title: 'Infini' }, { label: 'α', insert: '\\alpha', title: 'Alpha' }, { label: 'β', insert: '\\beta', title: 'Beta' }, { label: 'γ', insert: '\\gamma', title: 'Gamma' }, { label: 'π', insert: '\\pi', title: 'Pi' }, { label: 'θ', insert: '\\theta', title: 'Theta' }, { label: 'Δ', insert: '\\Delta', title: 'Delta' }, ] const MathEquationView = ({ node, updateAttributes, deleteNode, selected }: any) => { const { t } = useLanguage() const latex = node.attrs.latex || '' const [editing, setEditing] = useState(!latex) const [input, setInput] = useState(latex) const [aiLoading, setAiLoading] = useState(false) const [aiInput, setAiInput] = useState('') const [showAi, setShowAi] = useState(false) const inputRef = useRef(null) const renderRef = useRef(null) const { html } = renderKatex(latex, true) useEffect(() => { if (editing && inputRef.current) inputRef.current.focus() }, [editing]) useEffect(() => { if (renderRef.current && html) renderRef.current.innerHTML = html }, [html]) const commit = () => { updateAttributes({ latex: input }) if (input.trim()) setEditing(false) } const insertSymbol = (symbol: string) => { const el = inputRef.current if (!el) { setInput(prev => prev + symbol); return } const start = el.selectionStart const end = el.selectionEnd const newVal = input.slice(0, start) + symbol + input.slice(end) setInput(newVal) setTimeout(() => { el.focus() const cursorPos = start + symbol.length if (symbol.includes('{}')) { const braceIdx = symbol.indexOf('{') el.setSelectionRange(start + braceIdx + 1, start + braceIdx + 1) } else { el.setSelectionRange(cursorPos, cursorPos) } }, 0) } const handleAiGenerate = async () => { if (!aiInput.trim()) return setAiLoading(true) try { const res = await fetch('/api/ai/math-from-text', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: aiInput }), }) const data = await res.json() if (res.ok && data.latex) { setInput(data.latex) setShowAi(false) setAiInput('') } } catch (e) { console.error(e) } finally { setAiLoading(false) } } const livePreview = renderKatex(input, true).html return ( {editing ? (
{/* Toolbar */}
{MATH_BUTTONS.map(btn => ( ))}
{/* AI generation */} {showAi ? (
setAiInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAiGenerate() } }} placeholder={t('richTextEditor.mathAiPlaceholder')} className="flex-1 min-w-0 bg-transparent text-sm outline-none" autoFocus /> {aiLoading ? ( ) : ( )}
) : ( )} {/* LaTeX input */}