'use client' import { Node, mergeAttributes } from '@tiptap/core' import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps, } from '@tiptap/react' import type { Editor } from '@tiptap/core' import { InteractiveDemoPlayer } from '@/components/interactive-demo/interactive-demo-player' import { validateInteractiveDemo, type InteractiveDemoV1, } from '@/lib/interactive-demo' import attnresFixture from '@/lib/interactive-demo/fixtures/attnres.demo.json' import { AlertCircle } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { useMemo } from 'react' function InteractiveDemoView(props: NodeViewProps) { const { t } = useLanguage() const raw = props.node.attrs.demoJson as string const parsed = useMemo(() => { try { const data = JSON.parse(raw || '{}') return validateInteractiveDemo(data) } catch { return { ok: false as const, issues: [{ code: 'invalid_json', path: '', message: 'Invalid JSON' }], } } }, [raw]) if (!parsed.ok) { return (

{t('interactiveDemo.invalid') || 'Interactive demo invalide'}

    {parsed.issues.slice(0, 5).map((iss, i) => (
  • {iss.path ? `${iss.path}: ` : ''} {iss.message}
  • ))}
) } return ( ) } export const InteractiveDemoExtension = Node.create({ name: 'interactiveDemo', group: 'block', atom: true, draggable: true, selectable: true, addAttributes() { return { demoJson: { default: '{}', parseHTML: (el) => el.getAttribute('data-demo-json') || '{}', renderHTML: (attrs) => ({ 'data-demo-json': attrs.demoJson || '{}', }), }, } }, parseHTML() { return [{ tag: 'div[data-interactive-demo]' }] }, renderHTML({ HTMLAttributes }) { return [ 'div', mergeAttributes(HTMLAttributes, { 'data-interactive-demo': 'true' }), ] }, addNodeView() { return ReactNodeViewRenderer(InteractiveDemoView) }, }) export function insertInteractiveDemoAtSelection( editor: Editor, demo?: InteractiveDemoV1 ): boolean { const type = editor.schema.nodes.interactiveDemo if (!type) return false const payload = demo ?? (attnresFixture as InteractiveDemoV1) const check = validateInteractiveDemo(payload) // Prefer validated shape; if already-server-validated payload fails client Zod // (HMR drift), still insert so the block appears — NodeView shows issues if needed. const attrs = { demoJson: JSON.stringify(check.ok ? check.demo : payload), } const { empty, $from } = editor.state.selection const pos = empty ? $from.pos : editor.state.selection.from return editor .chain() .focus() .insertContentAt(pos, { type: 'interactiveDemo', attrs }) .run() }