Files
HaHafeng 303dd78c54 feat(aia): Protocol Agent MVP complete with one-click generation and Word export
- Add one-click research protocol generation with streaming output

- Implement Word document export via Pandoc integration

- Add dynamic dual-panel layout with resizable split pane

- Implement collapsible content for StatePanel stages

- Add conversation history management with title auto-update

- Fix scroll behavior, markdown rendering, and UI layout issues

- Simplify conversation creation logic for reliability
2026-01-25 19:16:36 +08:00

3.3 KiB
Raw Permalink Blame History

基于 Novel (Tiptap) 的 CRF 表单扩展开发指南

目标: 在编辑器中实现 "填空"、"单选"、"日期选择" 等 CRF 控件,并支持导出 Word。

1. 自定义 CRF 节点开发 (Tiptap Extensions)

我们需要开发一组 Node Extensions,让编辑器理解表单元素。

1.1 填空输入框 (UnderlineInput)

// extensions/underline-input.tsx
import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react';

export const UnderlineInput = Node.create({
name: 'underlineInput',
group: 'inline',
inline: true,
atom: true, // 作为一个整体,不可分割

addAttributes() {
return {
placeholder: { default: '请输入...' },
value: { default: '' },
}
},

parseHTML() {
return [{ tag: 'span[data-type="input"]' }]
},

renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(HTMLAttributes, { 'data-type': 'input' })]
},

// React 组件渲染
addNodeView() {
return ReactNodeViewRenderer(({ node, updateAttributes }) => {
return (
<NodeViewWrapper as="span" className="inline-block mx-1">
<input
className="border-b border-black outline-none px-1 w-24 bg-transparent text-center focus:border-blue-500"
placeholder={node.attrs.placeholder}
value={node.attrs.value}
onChange={(e) => updateAttributes({ value: e.target.value })}
/>
</NodeViewWrapper>
)
})
},
});

1.2 单选组 (RadioGroup)

// extensions/radio-group.tsx
// 类似逻辑,渲染一组 radio buttons

2. AI 生成与解析策略

如何让 AI 生成这些控件?我们约定一套 "占位符语法"

  • Prompt: "生成一个性别选择项,包含男、女。"
  • AI Output: 性别: {{radio:男,女}}
  • 前端解析 (Replacement Logic):

// 在 useAIStream 的 onStream 更新中
const parseContent = (text) => {
// 正则替换
if (text.includes('{{input}}')) {
editor.chain().focus().insertContent({ type: 'underlineInput' }).run();
}
// ...
};

3. Word 导出逻辑 (docx.js)

针对自定义节点的导出映射。

// utils/export-docx.ts
import { TextRun, UnderlineType } from "docx";

export const transformNode = (node) => {
switch (node.type) {

// 导出填空框 \-\> 带下划线的空格  
case 'underlineInput':  
  return new TextRun({  
    text: node.attrs.value || "          ", // 有值填值,无值填空格  
    underline: {  
      type: UnderlineType.SINGLE,  
    },  
  });

// 导出复选框 \-\> 特殊字符  
case 'taskItem':  
  const isChecked \= node.attrs.checked;  
  return new TextRun({  
    text: isChecked ? "☑ " : "☐ ", // Unicode 字符  
    font: "Arial Unicode MS", // 确保字体支持  
  });  
    
// ... 其他节点  

}
};

4. 总结

Novel (Tiptap) 完全有能力承载 CRF 的需求。

虽然这需要一些 "Extension 开发" 的工作量,但相比自己从头写一个 Form Builder这是性价比最高的方案而且还能保持文档的流式阅读体验。