feat(components): Add markdown editing capability and enhance component styling
- Add editable mode to Markdown component with edit/save/cancel buttons - Import EditOutlined, SaveOutlined, CloseOutlined icons from ant-design - Add useState, useRef, useEffect hooks for managing edit state - Add editable, onContentChange, and onSave props to RbMarkdownProps interface - Create RbModal component with new index.css stylesheet for modal styling - Add index.css stylesheet to KnowledgeBase components for consistent styling - Update i18n translations in en.ts and zh.ts for new UI elements - Refactor Markdown component handlers to accept and spread additional props - Update InsertModal and RecallTestResult components for improved UX - Fix prop spreading in component handlers to maintain compatibility with Ant Design components
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd'
|
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider, Button } from 'antd'
|
||||||
|
import { EditOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import RemarkGfm from 'remark-gfm'
|
import RemarkGfm from 'remark-gfm'
|
||||||
import RemarkMath from 'remark-math'
|
import RemarkMath from 'remark-math'
|
||||||
@@ -6,6 +7,7 @@ import RemarkBreaks from 'remark-breaks'
|
|||||||
import RehypeKatex from 'rehype-katex'
|
import RehypeKatex from 'rehype-katex'
|
||||||
import RehypeRaw from 'rehype-raw'
|
import RehypeRaw from 'rehype-raw'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
|
||||||
import Code from './Code'
|
import Code from './Code'
|
||||||
import VideoBlock from './VideoBlock'
|
import VideoBlock from './VideoBlock'
|
||||||
@@ -16,42 +18,45 @@ import RbButton from './RbButton'
|
|||||||
interface RbMarkdownProps {
|
interface RbMarkdownProps {
|
||||||
content: string;
|
content: string;
|
||||||
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏)
|
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏)
|
||||||
|
editable?: boolean; // 是否可编辑,默认为 false
|
||||||
|
onContentChange?: (content: string) => void; // 内容变化回调
|
||||||
|
onSave?: (content: string) => void; // 保存回调
|
||||||
}
|
}
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
h1: ({ children }: { children: string }) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2">{children}</h1>,
|
h1: ({ children, ...props }: any) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2" {...props}>{children}</h1>,
|
||||||
h2: ({ children }: { children: string }) => <h2 className="rb:text-xl rb:font-bold rb:mb-2">{children}</h2>,
|
h2: ({ children, ...props }: any) => <h2 className="rb:text-xl rb:font-bold rb:mb-2" {...props}>{children}</h2>,
|
||||||
h3: ({ children }: { children: string }) => <h3 className="rb:text-lg rb:font-bold rb:mb-2">{children}</h3>,
|
h3: ({ children, ...props }: any) => <h3 className="rb:text-lg rb:font-bold rb:mb-2" {...props}>{children}</h3>,
|
||||||
h4: ({ children }: { children: string }) => <h4 className="rb:text-md rb:font-bold rb:mb-2">{children}</h4>,
|
h4: ({ children, ...props }: any) => <h4 className="rb:text-md rb:font-bold rb:mb-2" {...props}>{children}</h4>,
|
||||||
h5: ({ children }: { children: string }) => <h5 className="rb:text-sm rb:font-bold rb:mb-2">{children}</h5>,
|
h5: ({ children, ...props }: any) => <h5 className="rb:text-sm rb:font-bold rb:mb-2" {...props}>{children}</h5>,
|
||||||
h6: ({ children }: { children: string }) => <h6 className="rb:text-xs rb:font-bold rb:mb-2">{children}</h6>,
|
h6: ({ children, ...props }: any) => <h6 className="rb:text-xs rb:font-bold rb:mb-2" {...props}>{children}</h6>,
|
||||||
ul: ({ children }: { children: string }) => <ul className="rb:list-disc rb:ml-6 rb:mb-2">{children}</ul>,
|
ul: ({ children, ...props }: any) => <ul className="rb:list-disc rb:ml-6 rb:mb-2" {...props}>{children}</ul>,
|
||||||
ol: ({ children }: { children: string }) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2">{children}</ol>,
|
ol: ({ children, ...props }: any) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2" {...props}>{children}</ol>,
|
||||||
li: ({ children }: { children: string }) => <li className="rb:mb-1">{children}</li>,
|
li: ({ children, ...props }: any) => <li className="rb:mb-1" {...props}>{children}</li>,
|
||||||
blockquote: ({ children }: { children: string }) => <blockquote className="rb:border-l-4 rb:border-[#D9D9D9] rb:pl-4 rb:mb-2">{children}</blockquote>,
|
blockquote: ({ children, ...props }: any) => <blockquote className="rb:border-l-4 rb:border-[#D9D9D9] rb:pl-4 rb:mb-2" {...props}>{children}</blockquote>,
|
||||||
p: ({ children }: { children: string }) => <p className="rb:mb-2">{children}</p>,
|
p: ({ children, ...props }: any) => <p className="rb:mb-2" {...props}>{children}</p>,
|
||||||
strong: ({ children }: { children: string }) => <strong className="rb:font-bold">{children}</strong>,
|
strong: ({ children, ...props }: any) => <strong className="rb:font-bold" {...props}>{children}</strong>,
|
||||||
em: ({ children }: { children: string }) => <em className="rb:italic">{children}</em>,
|
em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>,
|
||||||
del: ({ children }: { children: string }) => <del className="rb:line-through">{children}</del>,
|
del: ({ children, ...props }: any) => <del className="rb:line-through" {...props}>{children}</del>,
|
||||||
span: ({ children, ...props }: any) => {
|
span: ({ children, style, ...restProps }: any) => {
|
||||||
// 如果是 HTML 注释的 span,应用特殊样式
|
// 如果是 HTML 注释的 span,应用特殊样式
|
||||||
if (props.style?.color === '#999') {
|
if (style?.color === '#999') {
|
||||||
return <span style={{ color: '#999', fontSize: '0.9em' }}>{children}</span>
|
return <span style={{ color: '#999', fontSize: '0.9em' }}>{children}</span>
|
||||||
}
|
}
|
||||||
return <span {...props}>{children}</span>
|
return <span style={style} {...restProps}>{children}</span>
|
||||||
},
|
},
|
||||||
|
|
||||||
code: Code,
|
code: ({ children, className, ...props }: any) => <Code children={String(children)} className={className || ''} {...props} />,
|
||||||
img: Image,
|
img: ({ src, alt, ...props }: any) => <Image src={src} alt={alt} {...props} />,
|
||||||
video: VideoBlock,
|
video: ({ src, ...props }: any) => <VideoBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
|
||||||
audio: AudioBlock,
|
audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
|
||||||
a: Link,
|
a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>,
|
||||||
button: RbButton,
|
button: ({ children, ...props }: any) => <RbButton node={{ children }}>{[children]}</RbButton>,
|
||||||
table: ({ children }: { children: string }) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2">{children}</table>,
|
table: ({ children, ...props }: any) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table>,
|
||||||
tr: ({ children }: { children: string }) => <tr className="rb:border rb:border-[#D9D9D9]">{children}</tr>,
|
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>,
|
||||||
th: ({ children }: { children: string }) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold">{children}</th>,
|
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
|
||||||
td: ({ children }: { children: string }) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left">{children}</td>,
|
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,
|
||||||
input: ({ children, ...props }: { children: string }) => {
|
input: ({ children, ...props }: any) => {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'color':
|
case 'color':
|
||||||
return <ColorPicker {...props} />
|
return <ColorPicker {...props} />
|
||||||
@@ -74,7 +79,7 @@ const components = {
|
|||||||
return <Slider {...props} />
|
return <Slider {...props} />
|
||||||
case 'submit':
|
case 'submit':
|
||||||
case 'button':
|
case 'button':
|
||||||
return <RbButton {...props}>{props.value}</RbButton>
|
return <RbButton node={{ children: props.value || children }}>{[props.value || children]}</RbButton>
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return <Checkbox {...props}>{children}</Checkbox>
|
return <Checkbox {...props}>{children}</Checkbox>
|
||||||
case 'password':
|
case 'password':
|
||||||
@@ -85,37 +90,158 @@ const components = {
|
|||||||
return <Input value={children} {...props} />
|
return <Input value={children} {...props} />
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
select: ({ children, ...props }: { children: string }) => <Select style={{width: '100%'}} {...props}>{children}</Select>,
|
select: ({ children, ...props }: any) => <Select style={{width: '100%'}} {...props}>{children}</Select>,
|
||||||
textarea: ({ children, ...props }: { children: string }) => <Input.TextArea {...props}>{children}</Input.TextArea>,
|
textarea: ({ children, ...props }: any) => <Input.TextArea {...props}>{children}</Input.TextArea>,
|
||||||
form: ({ children }: { children: string }) => <Form>{children}</Form>,
|
form: ({ children, ...props }: any) => <Form {...props}>{children}</Form>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const RbMarkdown: FC<RbMarkdownProps> = ({
|
const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||||
content,
|
content,
|
||||||
showHtmlComments = false,
|
showHtmlComments = false,
|
||||||
|
editable = false,
|
||||||
|
onContentChange,
|
||||||
|
onSave,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isEditing, setIsEditing] = useState(editable) // 如果可编辑,默认进入编辑模式
|
||||||
|
const [editContent, setEditContent] = useState(content)
|
||||||
|
const textareaRef = useRef<any>(null)
|
||||||
|
|
||||||
|
// 当外部 content 变化时,同步更新编辑内容
|
||||||
|
useEffect(() => {
|
||||||
|
setEditContent(content)
|
||||||
|
}, [content])
|
||||||
|
|
||||||
|
// 当editable变化时,自动切换编辑状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (editable) {
|
||||||
|
setIsEditing(true)
|
||||||
|
// 延迟聚焦,确保 textarea 已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}, [editable])
|
||||||
|
|
||||||
|
// 进入编辑模式
|
||||||
|
const handleEdit = () => {
|
||||||
|
setIsEditing(true)
|
||||||
|
setEditContent(content)
|
||||||
|
// 延迟聚焦,确保 textarea 已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存编辑
|
||||||
|
const handleSave = () => {
|
||||||
|
onContentChange?.(editContent)
|
||||||
|
onSave?.(editContent)
|
||||||
|
if (!editable) {
|
||||||
|
setIsEditing(false) // 只有在非强制编辑模式下才退出编辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditContent(content) // 恢复原内容
|
||||||
|
if (!editable) {
|
||||||
|
setIsEditing(false) // 只有在非强制编辑模式下才退出编辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 textarea 内容变化
|
||||||
|
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newContent = e.target.value
|
||||||
|
setEditContent(newContent)
|
||||||
|
// 实时回调内容变化
|
||||||
|
onContentChange?.(newContent)
|
||||||
|
}
|
||||||
|
|
||||||
// 根据参数决定是否将 HTML 注释转换为可见文本
|
// 根据参数决定是否将 HTML 注释转换为可见文本
|
||||||
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
|
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
|
||||||
const processedContent = showHtmlComments
|
const processedContent = showHtmlComments
|
||||||
? content.replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
|
? (isEditing ? editContent : content).replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
|
||||||
// 转换为带样式的文本,使用 <span class="html-comment"> 标记
|
// 转换为带样式的文本,使用 <span class="html-comment"> 标记
|
||||||
const escaped = commentContent.trim().replace(/</g, '<').replace(/>/g, '>')
|
const escaped = commentContent.trim().replace(/</g, '<').replace(/>/g, '>')
|
||||||
return `<span class="html-comment"><!-- ${escaped} --></span>`
|
return `<span class="html-comment"><!-- ${escaped} --></span>`
|
||||||
})
|
})
|
||||||
: content
|
: (isEditing ? editContent : content)
|
||||||
|
|
||||||
|
// 如果是编辑模式,显示 textarea
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<div className="rb:relative">
|
||||||
|
<style>{`
|
||||||
|
.html-comment {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* 编辑工具栏 - 只在非强制编辑模式下显示 */}
|
||||||
|
{!editable && (
|
||||||
|
<div className="rb:flex rb:justify-end rb:gap-2 rb:mb-2">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 编辑区域 */}
|
||||||
|
<Input.TextArea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={editContent}
|
||||||
|
onChange={handleTextareaChange}
|
||||||
|
rows={editable ? 5 : 10}
|
||||||
|
className="rb:font-mono rb:text-sm"
|
||||||
|
placeholder="请输入 Markdown 内容..."
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览模式
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="rb:relative rb:group">
|
||||||
<style>{`
|
<style>{`
|
||||||
.html-comment {
|
.html-comment {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
|
{/* 编辑按钮 - 只在非强制编辑模式且鼠标悬停时显示 */}
|
||||||
|
{!editable && (
|
||||||
|
<div className="rb:absolute rb:top-0 rb:right-0 rb:opacity-0 group-hover:rb:opacity-100 rb:transition-opacity rb:z-10">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={handleEdit}
|
||||||
|
className="rb:bg-white rb:shadow-sm"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
// allowElement={[]}
|
// allowElement={[]}
|
||||||
// allowedElements={[]}
|
// allowedElements={[]}
|
||||||
components={components}
|
components={components as any}
|
||||||
disallowedElements={['script', 'iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
|
disallowedElements={['script', 'iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
|
||||||
rehypePlugins={[
|
rehypePlugins={[
|
||||||
RehypeKatex,
|
RehypeKatex,
|
||||||
|
|||||||
3
web/src/components/RbModal/index.css
Normal file
3
web/src/components/RbModal/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.rb-modal .ant-modal-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* @Description:
|
||||||
|
* @Version: 0.0.1
|
||||||
|
* @Author: yujiangping
|
||||||
|
* @Date: 2025-12-16 10:19:18
|
||||||
|
* @LastEditors: yujiangping
|
||||||
|
* @LastEditTime: 2025-12-22 12:31:31
|
||||||
|
*/
|
||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import { Modal, type ModalProps } from 'antd'
|
import { Modal, type ModalProps } from 'antd'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const RbModal: FC<ModalProps> = ({
|
const RbModal: FC<ModalProps> = ({
|
||||||
onOk,
|
onOk,
|
||||||
onCancel,
|
onCancel,
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -16,6 +24,7 @@ const RbModal: FC<ModalProps> = ({
|
|||||||
cancelText={t('common.cancel')}
|
cancelText={t('common.cancel')}
|
||||||
onOk={onOk}
|
onOk={onOk}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
|
className={`rb-modal ${className || ''}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className='rb:max-h-[550px] rb:overflow-y-auto rb:overflow-x-hidden'>
|
<div className='rb:max-h-[550px] rb:overflow-y-auto rb:overflow-x-hidden'>
|
||||||
|
|||||||
@@ -585,7 +585,6 @@ export const en = {
|
|||||||
insertContent: 'Insert Content',
|
insertContent: 'Insert Content',
|
||||||
editContent:'Edit Content',
|
editContent:'Edit Content',
|
||||||
insertContentPlaceholder: 'Please enter the content',
|
insertContentPlaceholder: 'Please enter the content',
|
||||||
pleaseEnterContent: 'Please enter content',
|
|
||||||
documentIdRequired: 'Document ID is required',
|
documentIdRequired: 'Document ID is required',
|
||||||
editContentDesc:'Edit content',
|
editContentDesc:'Edit content',
|
||||||
insertContentDesc:'Insert content',
|
insertContentDesc:'Insert content',
|
||||||
@@ -598,6 +597,10 @@ export const en = {
|
|||||||
semantic:'Semantic',
|
semantic:'Semantic',
|
||||||
hybrid:'Hybrid',
|
hybrid:'Hybrid',
|
||||||
updateEmbeddingContent:'Are you sure about updating the embedding model? After the update, will the block vector data need to be reconstructed?',
|
updateEmbeddingContent:'Are you sure about updating the embedding model? After the update, will the block vector data need to be reconstructed?',
|
||||||
|
question: 'Question',
|
||||||
|
answer: 'Answer',
|
||||||
|
normalMode: 'Normal Mode',
|
||||||
|
qaMode: 'QA Mode',
|
||||||
createForm:{
|
createForm:{
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
embedding_id: 'Embedding',
|
embedding_id: 'Embedding',
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ export const zh = {
|
|||||||
insertContent: '插入内容',
|
insertContent: '插入内容',
|
||||||
editContent: '编辑内容',
|
editContent: '编辑内容',
|
||||||
insertContentPlaceholder: '请输入内容',
|
insertContentPlaceholder: '请输入内容',
|
||||||
pleaseEnterContent: '请输入内容',
|
|
||||||
documentIdRequired: '文档ID是必需的',
|
documentIdRequired: '文档ID是必需的',
|
||||||
editContentDesc: '编辑内容',
|
editContentDesc: '编辑内容',
|
||||||
insertContentDesc: '插入内容',
|
insertContentDesc: '插入内容',
|
||||||
@@ -229,6 +228,10 @@ export const zh = {
|
|||||||
semantic: '语义',
|
semantic: '语义',
|
||||||
hybrid: '混合',
|
hybrid: '混合',
|
||||||
updateEmbeddingContent: '确定要更新嵌入模型吗?更新后,分块向量数据需要重新构建?',
|
updateEmbeddingContent: '确定要更新嵌入模型吗?更新后,分块向量数据需要重新构建?',
|
||||||
|
question: '问题',
|
||||||
|
answer: '答案',
|
||||||
|
normalMode: '常规模式',
|
||||||
|
qaMode: '问答模式',
|
||||||
createForm: {
|
createForm: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
embedding_id: '嵌入模型',
|
embedding_id: '嵌入模型',
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
import { Input, message, Tabs } from 'antd';
|
import { message, Tabs } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import RbModal from '@/components/RbModal';
|
import RbModal from '@/components/RbModal';
|
||||||
import RbMarkdown from '@/components/Markdown';
|
import RbMarkdown from '@/components/Markdown';
|
||||||
|
import './index.css'
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
export interface InsertModalRef {
|
export interface InsertModalRef {
|
||||||
handleOpen: (documentId: string, initialContent?: string, chunkId?: string) => void;
|
handleOpen: (documentId: string, initialContent?: string, chunkId?: string) => void;
|
||||||
@@ -24,11 +23,14 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
|||||||
const [content, setContent] = useState<string>('');
|
const [content, setContent] = useState<string>('');
|
||||||
const [chunkId, setChunkId] = useState<string | undefined>(undefined);
|
const [chunkId, setChunkId] = useState<string | undefined>(undefined);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<string>('edit');
|
const [activeTab, setActiveTab] = useState<string>('normalMode');
|
||||||
|
const [mode, setMode] = useState(0); // 0: 普通模式, 1: QA模式
|
||||||
|
const [question, setQuestion] = useState<string>('');
|
||||||
|
const [answer, setAnswer] = useState<string>('');
|
||||||
|
|
||||||
const handleOpen = (docId: string, initialContent?: string, chunkIdParam?: string) => {
|
const handleOpen = (docId: string, initialContent?: string, chunkIdParam?: string) => {
|
||||||
setDocumentId(docId);
|
setDocumentId(docId);
|
||||||
setContent(initialContent || '');
|
handleContent(initialContent || '')
|
||||||
setChunkId(chunkIdParam);
|
setChunkId(chunkIdParam);
|
||||||
setIsEditMode(!!initialContent);
|
setIsEditMode(!!initialContent);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
@@ -40,11 +42,63 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
|||||||
setDocumentId('');
|
setDocumentId('');
|
||||||
setChunkId(undefined);
|
setChunkId(undefined);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
setActiveTab('edit');
|
setActiveTab('normalMode');
|
||||||
|
setMode(0);
|
||||||
|
setQuestion('');
|
||||||
|
setAnswer('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析 QA 格式内容
|
||||||
|
const parseQAContent = (content: string) => {
|
||||||
|
if (!content) return null;
|
||||||
|
|
||||||
|
const qaRegex = /question:\s*(.*?)\s*answer:\s*(.*?)$/s;
|
||||||
|
const match = content.match(qaRegex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const question = match[1]?.trim() || '';
|
||||||
|
const answer = match[2]?.trim() || '';
|
||||||
|
return { question, answer };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContent = (value: string) => {
|
||||||
|
if (value === '') return;
|
||||||
|
const qaContent = parseQAContent(value);
|
||||||
|
if (qaContent) {
|
||||||
|
setMode(1); // 1 表示 QA 模式
|
||||||
|
setQuestion(qaContent.question);
|
||||||
|
setAnswer(qaContent.answer);
|
||||||
|
setContent(qaContent.answer); // 保持原始内容用于提交
|
||||||
|
setActiveTab('qaMode')
|
||||||
|
} else {
|
||||||
|
setMode(0);
|
||||||
|
setAnswer(value)
|
||||||
|
setContent(value);
|
||||||
|
setActiveTab('normalMode')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleTabsChange = (key: string) => {
|
||||||
|
if(key === 'qaMode'){
|
||||||
|
setMode(1);
|
||||||
|
}else{
|
||||||
|
setMode(0);
|
||||||
|
}
|
||||||
|
setActiveTab(key);
|
||||||
|
};
|
||||||
|
// 获取当前要提交的内容
|
||||||
|
const getCurrentContent = () => {
|
||||||
|
if (mode === 1) {
|
||||||
|
return `question: ${question}\n answer: ${answer}`;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
if (!content.trim()) {
|
const currentContent = getCurrentContent();
|
||||||
|
if (!currentContent.trim()) {
|
||||||
message.warning(t('knowledgeBase.pleaseEnterContent') || '请输入内容');
|
message.warning(t('knowledgeBase.pleaseEnterContent') || '请输入内容');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,7 +111,7 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (onInsert) {
|
if (onInsert) {
|
||||||
const success = await onInsert(documentId, content.trim(), chunkId);
|
const success = await onInsert(documentId, currentContent.trim(), chunkId);
|
||||||
if (success) {
|
if (success) {
|
||||||
const successMsg = isEditMode
|
const successMsg = isEditMode
|
||||||
? (t('knowledgeBase.updateSuccess') || '更新成功')
|
? (t('knowledgeBase.updateSuccess') || '更新成功')
|
||||||
@@ -86,47 +140,70 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setContent(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
// 暴露给父组件的方法
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleOpen,
|
handleOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 构建标签页项目,content 为空或新增时不显示预览
|
// 构建标签页项目
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
key: 'edit',
|
key: 'normalMode',
|
||||||
label: t('knowledgeBase.edit') || '编辑',
|
label: t('knowledgeBase.normalMode'),
|
||||||
children: (
|
children: (
|
||||||
<TextArea
|
// <div className='rb:border rb:border-[#D9D9D9] rb:rounded rb:p-4 rb:min-h-[280px] rb:max-h-[400px] rb:overflow-y-auto rb:bg-white'>
|
||||||
value={content}
|
<RbMarkdown
|
||||||
onChange={handleContentChange}
|
content={content}
|
||||||
placeholder={t('knowledgeBase.insertContentPlaceholder') || '请输入内容...'}
|
showHtmlComments={true}
|
||||||
rows={10}
|
editable={true}
|
||||||
maxLength={10000}
|
onContentChange={setContent}
|
||||||
showCount
|
onSave={(newContent) => {
|
||||||
autoFocus
|
setContent(newContent);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
// </div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
key: 'qaMode',
|
||||||
// 只有在编辑模式且有内容时才显示预览标签页
|
label: t('knowledgeBase.qaMode'),
|
||||||
if (isEditMode && content) {
|
|
||||||
tabItems.push({
|
|
||||||
key: 'preview',
|
|
||||||
label: t('knowledgeBase.preview') || '预览',
|
|
||||||
children: (
|
children: (
|
||||||
<div className='rb:border rb:border-[#D9D9D9] rb:rounded rb:p-4 rb:min-h-[280px] rb:max-h-[400px] rb:overflow-y-auto rb:bg-white'>
|
// QA 模式的编辑界面
|
||||||
<RbMarkdown content={content} showHtmlComments={true} />
|
<div className='rb:flex rb:flex-col rb:gap-4'>
|
||||||
|
<div>
|
||||||
|
<div className='rb:w-full rb:font-medium rb:leading-8 rb:mb-2'>{t('knowledgeBase.question') || '问题'}</div>
|
||||||
|
{/* <div className='rb:border rb:border-[#D9D9D9] rb:rounded rb:p-4 rb:min-h-[120px] rb:max-h-[200px] rb:overflow-y-auto rb:bg-white'> */}
|
||||||
|
<RbMarkdown
|
||||||
|
content={question}
|
||||||
|
showHtmlComments={true}
|
||||||
|
editable={true}
|
||||||
|
onContentChange={setQuestion}
|
||||||
|
onSave={(newContent) => {
|
||||||
|
setQuestion(newContent);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* </div> */}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='rb:w-full rb:font-medium rb:leading-8 rb:mb-2'>{t('knowledgeBase.answer') || '答案'}</div>
|
||||||
|
{/* <div className='rb:border rb:border-[#D9D9D9] rb:rounded rb:p-4 rb:min-h-[120px] rb:max-h-[200px] rb:overflow-y-auto rb:bg-white'> */}
|
||||||
|
<RbMarkdown
|
||||||
|
content={answer}
|
||||||
|
showHtmlComments={true}
|
||||||
|
editable={true}
|
||||||
|
onContentChange={setAnswer}
|
||||||
|
onSave={(newContent) => {
|
||||||
|
setAnswer(newContent);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* </div> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
)
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbModal
|
<RbModal
|
||||||
@@ -141,11 +218,12 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
|||||||
okText={t('common.confirm') || '确认'}
|
okText={t('common.confirm') || '确认'}
|
||||||
cancelText={t('common.cancel') || '取消'}
|
cancelText={t('common.cancel') || '取消'}
|
||||||
width={600}
|
width={600}
|
||||||
|
className='rb:h-[800px]'
|
||||||
>
|
>
|
||||||
<div className='rb:flex rb:flex-col rb:gap-4'>
|
<div className='rb:flex rb:flex-col rb:gap-4'>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={handleTabsChange}
|
||||||
items={tabItems}
|
items={tabItems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,4 +231,4 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default InsertModal;
|
export default InsertModal;
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* @Author: yujiangping
|
* @Author: yujiangping
|
||||||
* @Date: 2025-11-18 16:19:58
|
* @Date: 2025-11-18 16:19:58
|
||||||
* @LastEditors: yujiangping
|
* @LastEditors: yujiangping
|
||||||
* @LastEditTime: 2025-11-29 19:08:40
|
* @LastEditTime: 2025-12-22 13:47:53
|
||||||
*/
|
*/
|
||||||
import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons';
|
import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
import { Skeleton } from 'antd';
|
import { Skeleton } from 'antd';
|
||||||
@@ -58,7 +58,7 @@ const RecallTestResult = ({
|
|||||||
|
|
||||||
// 格式化 QA 内容为显示格式
|
// 格式化 QA 内容为显示格式
|
||||||
const formatQAContent = (question: string, answer: string) => {
|
const formatQAContent = (question: string, answer: string) => {
|
||||||
return `**问题:** ${question}\n\n**答案:** ${answer}`;
|
return `**${t('knowledgeBase.question')}:** ${question}\n**${t('knowledgeBase.answer')}:** ${answer}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => {
|
const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => {
|
||||||
|
|||||||
3
web/src/views/KnowledgeBase/components/index.css
Normal file
3
web/src/views/KnowledgeBase/components/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user