diff --git a/web/src/components/Markdown/index.tsx b/web/src/components/Markdown/index.tsx index 12e4235d..fdaec143 100644 --- a/web/src/components/Markdown/index.tsx +++ b/web/src/components/Markdown/index.tsx @@ -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 RemarkGfm from 'remark-gfm' import RemarkMath from 'remark-math' @@ -6,6 +7,7 @@ import RemarkBreaks from 'remark-breaks' import RehypeKatex from 'rehype-katex' import RehypeRaw from 'rehype-raw' import type { FC } from 'react' +import { useState, useRef, useEffect } from 'react' import Code from './Code' import VideoBlock from './VideoBlock' @@ -16,42 +18,45 @@ import RbButton from './RbButton' interface RbMarkdownProps { content: string; showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏) + editable?: boolean; // 是否可编辑,默认为 false + onContentChange?: (content: string) => void; // 内容变化回调 + onSave?: (content: string) => void; // 保存回调 } const components = { - h1: ({ children }: { children: string }) =>

{children}

, - h2: ({ children }: { children: string }) =>

{children}

, - h3: ({ children }: { children: string }) =>

{children}

, - h4: ({ children }: { children: string }) =>

{children}

, - h5: ({ children }: { children: string }) =>
{children}
, - h6: ({ children }: { children: string }) =>
{children}
, - ul: ({ children }: { children: string }) => , - ol: ({ children }: { children: string }) =>
    {children}
, - li: ({ children }: { children: string }) =>
  • {children}
  • , - blockquote: ({ children }: { children: string }) =>
    {children}
    , - p: ({ children }: { children: string }) =>

    {children}

    , - strong: ({ children }: { children: string }) => {children}, - em: ({ children }: { children: string }) => {children}, - del: ({ children }: { children: string }) => {children}, - span: ({ children, ...props }: any) => { + h1: ({ children, ...props }: any) =>

    {children}

    , + h2: ({ children, ...props }: any) =>

    {children}

    , + h3: ({ children, ...props }: any) =>

    {children}

    , + h4: ({ children, ...props }: any) =>

    {children}

    , + h5: ({ children, ...props }: any) =>
    {children}
    , + h6: ({ children, ...props }: any) =>
    {children}
    , + ul: ({ children, ...props }: any) => , + ol: ({ children, ...props }: any) =>
      {children}
    , + li: ({ children, ...props }: any) =>
  • {children}
  • , + blockquote: ({ children, ...props }: any) =>
    {children}
    , + p: ({ children, ...props }: any) =>

    {children}

    , + strong: ({ children, ...props }: any) => {children}, + em: ({ children, ...props }: any) => {children}, + del: ({ children, ...props }: any) => {children}, + span: ({ children, style, ...restProps }: any) => { // 如果是 HTML 注释的 span,应用特殊样式 - if (props.style?.color === '#999') { + if (style?.color === '#999') { return {children} } - return {children} + return {children} }, - code: Code, - img: Image, - video: VideoBlock, - audio: AudioBlock, - a: Link, - button: RbButton, - table: ({ children }: { children: string }) => {children}
    , - tr: ({ children }: { children: string }) => {children}, - th: ({ children }: { children: string }) => {children}, - td: ({ children }: { children: string }) => {children}, - input: ({ children, ...props }: { children: string }) => { + code: ({ children, className, ...props }: any) => , + img: ({ src, alt, ...props }: any) => {alt}, + video: ({ src, ...props }: any) => , + audio: ({ src, ...props }: any) => , + a: ({ href, children, ...props }: any) => {children}, + button: ({ children, ...props }: any) => {[children]}, + table: ({ children, ...props }: any) => {children}
    , + tr: ({ children, ...props }: any) => {children}, + th: ({ children, ...props }: any) => {children}, + td: ({ children, ...props }: any) => {children}, + input: ({ children, ...props }: any) => { switch (props.type) { case 'color': return @@ -74,7 +79,7 @@ const components = { return case 'submit': case 'button': - return {props.value} + return {[props.value || children]} case 'checkbox': return {children} case 'password': @@ -85,37 +90,158 @@ const components = { return } }, - select: ({ children, ...props }: { children: string }) => , - textarea: ({ children, ...props }: { children: string }) => {children}, - form: ({ children }: { children: string }) =>
    {children}
    , + select: ({ children, ...props }: any) => , + textarea: ({ children, ...props }: any) => {children}, + form: ({ children, ...props }: any) =>
    {children}
    , } const RbMarkdown: FC = ({ content, showHtmlComments = false, + editable = false, + onContentChange, + onSave, }) => { + const [isEditing, setIsEditing] = useState(editable) // 如果可编辑,默认进入编辑模式 + const [editContent, setEditContent] = useState(content) + const textareaRef = useRef(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) => { + const newContent = e.target.value + setEditContent(newContent) + // 实时回调内容变化 + onContentChange?.(newContent) + } + // 根据参数决定是否将 HTML 注释转换为可见文本 // 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤 const processedContent = showHtmlComments - ? content.replace(//g, (_match, commentContent) => { + ? (isEditing ? editContent : content).replace(//g, (_match, commentContent) => { // 转换为带样式的文本,使用 标记 const escaped = commentContent.trim().replace(//g, '>') return `<!-- ${escaped} -->` }) - : content + : (isEditing ? editContent : content) + // 如果是编辑模式,显示 textarea + if (isEditing) { + return ( +
    + + + {/* 编辑工具栏 - 只在非强制编辑模式下显示 */} + {!editable && ( +
    + + +
    + )} + + {/* 编辑区域 */} + +
    + ) + } + + // 预览模式 return ( -
    +
    + + {/* 编辑按钮 - 只在非强制编辑模式且鼠标悬停时显示 */} + {!editable && ( +
    + +
    + )} + = ({ onOk, onCancel, children, + className, ...props }) => { const { t } = useTranslation() @@ -16,6 +24,7 @@ const RbModal: FC = ({ cancelText={t('common.cancel')} onOk={onOk} destroyOnHidden={true} + className={`rb-modal ${className || ''}`} {...props} >
    diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 503f54f0..d4877da9 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -585,7 +585,6 @@ export const en = { insertContent: 'Insert Content', editContent:'Edit Content', insertContentPlaceholder: 'Please enter the content', - pleaseEnterContent: 'Please enter content', documentIdRequired: 'Document ID is required', editContentDesc:'Edit content', insertContentDesc:'Insert content', @@ -598,6 +597,10 @@ export const en = { semantic:'Semantic', hybrid:'Hybrid', 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:{ name: 'Name', embedding_id: 'Embedding', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 1f9b315e..e78d1c45 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -216,7 +216,6 @@ export const zh = { insertContent: '插入内容', editContent: '编辑内容', insertContentPlaceholder: '请输入内容', - pleaseEnterContent: '请输入内容', documentIdRequired: '文档ID是必需的', editContentDesc: '编辑内容', insertContentDesc: '插入内容', @@ -229,6 +228,10 @@ export const zh = { semantic: '语义', hybrid: '混合', updateEmbeddingContent: '确定要更新嵌入模型吗?更新后,分块向量数据需要重新构建?', + question: '问题', + answer: '答案', + normalMode: '常规模式', + qaMode: '问答模式', createForm: { name: '名称', embedding_id: '嵌入模型', diff --git a/web/src/views/KnowledgeBase/components/InsertModal.tsx b/web/src/views/KnowledgeBase/components/InsertModal.tsx index 9f035998..7e08a1c4 100644 --- a/web/src/views/KnowledgeBase/components/InsertModal.tsx +++ b/web/src/views/KnowledgeBase/components/InsertModal.tsx @@ -1,10 +1,9 @@ import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Input, message, Tabs } from 'antd'; +import { message, Tabs } from 'antd'; import { useTranslation } from 'react-i18next'; import RbModal from '@/components/RbModal'; import RbMarkdown from '@/components/Markdown'; - -const { TextArea } = Input; +import './index.css' export interface InsertModalRef { handleOpen: (documentId: string, initialContent?: string, chunkId?: string) => void; @@ -24,11 +23,14 @@ const InsertModal = forwardRef(({ onInsert, on const [content, setContent] = useState(''); const [chunkId, setChunkId] = useState(undefined); const [isEditMode, setIsEditMode] = useState(false); - const [activeTab, setActiveTab] = useState('edit'); + const [activeTab, setActiveTab] = useState('normalMode'); + const [mode, setMode] = useState(0); // 0: 普通模式, 1: QA模式 + const [question, setQuestion] = useState(''); + const [answer, setAnswer] = useState(''); const handleOpen = (docId: string, initialContent?: string, chunkIdParam?: string) => { setDocumentId(docId); - setContent(initialContent || ''); + handleContent(initialContent || '') setChunkId(chunkIdParam); setIsEditMode(!!initialContent); setVisible(true); @@ -40,11 +42,63 @@ const InsertModal = forwardRef(({ onInsert, on setDocumentId(''); setChunkId(undefined); 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 () => { - if (!content.trim()) { + const currentContent = getCurrentContent(); + if (!currentContent.trim()) { message.warning(t('knowledgeBase.pleaseEnterContent') || '请输入内容'); return; } @@ -57,7 +111,7 @@ const InsertModal = forwardRef(({ onInsert, on setLoading(true); try { if (onInsert) { - const success = await onInsert(documentId, content.trim(), chunkId); + const success = await onInsert(documentId, currentContent.trim(), chunkId); if (success) { const successMsg = isEditMode ? (t('knowledgeBase.updateSuccess') || '更新成功') @@ -86,47 +140,70 @@ const InsertModal = forwardRef(({ onInsert, on } }; - const handleContentChange = (e: React.ChangeEvent) => { - setContent(e.target.value); - }; - // 暴露给父组件的方法 useImperativeHandle(ref, () => ({ handleOpen, handleClose, })); - // 构建标签页项目,content 为空或新增时不显示预览 + // 构建标签页项目 const tabItems = [ - { - key: 'edit', - label: t('knowledgeBase.edit') || '编辑', + { + key: 'normalMode', + label: t('knowledgeBase.normalMode'), children: ( -