feat(write): 文档撰写模式,支持编辑内容的自动保存和加载

- 移除未使用的 aiAssistantConfig 文件
- 实现草稿自动保存和加载功能
This commit is contained in:
zstar 2025-06-07 17:16:36 +08:00
parent 70647d36d5
commit 3a1bf079fc
4 changed files with 120 additions and 204 deletions

View File

@ -51,7 +51,7 @@ MYSQL_PORT=5455
# The hostname where the MinIO service is exposed # The hostname where the MinIO service is exposed
MINIO_HOST=minio MINIO_HOST=minio
# minio上传文件时的ip地址如需公网访问可修改为公网ip地址 # minio上传文件时的ip地址如需公网访问可修改为公网ip地址
MINIO_VISIT_HOST=host.docker.internal MINIO_VISIT_HOST=localhost
# The port used to expose the MinIO console interface to the host machine, # The port used to expose the MinIO console interface to the host machine,
# allowing EXTERNAL access to the web-based console running inside the Docker container. # allowing EXTERNAL access to the web-based console running inside the Docker container.
MINIO_CONSOLE_PORT=9001 MINIO_CONSOLE_PORT=9001

View File

@ -6,7 +6,6 @@
.messageContainer { .messageContainer {
overflow-y: auto; overflow-y: auto;
padding-right: 24px; padding-right: 24px;
h1, h1,
h2, h2,
h3, h3,

View File

@ -1,20 +0,0 @@
export const aiAssistantConfig = {
// 模型配置
model: {
modelName: "deepseek-r1:1.5b", // 默认模型
temperature: 0.7, // 温度参数
topP: 0.9, // Top-P 参数
maxTokens: 1024, // 最大生成 token 数
},
// API 配置
api: {
// 修改为正确的 API 端点格式
chatEndpoint: "/api/v1/chats", // 创建聊天会话的端点
completionEndpoint: "/api/v1/chats/{chat_id}/completions", // 聊天完成的端点
timeout: 300000, // 请求超时时间(ms)
},
// 默认系统提示词
systemPrompt: "你是一个专业的写作助手,可以帮助用户改进文档、提供写作建议和回答相关问题。",
};

View File

@ -39,9 +39,12 @@ import { useCallback, useEffect, useRef, useState } from 'react';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
const { Option } = Select; const { Option } = Select;
// --- KEY DEFINITIONS ---
const LOCAL_STORAGE_TEMPLATES_KEY = 'userWriteTemplates_v4_no_restore_final'; const LOCAL_STORAGE_TEMPLATES_KEY = 'userWriteTemplates_v4_no_restore_final';
const LOCAL_STORAGE_INIT_FLAG_KEY = const LOCAL_STORAGE_INIT_FLAG_KEY =
'userWriteTemplates_initialized_v4_no_restore_final'; 'userWriteTemplates_initialized_v4_no_restore_final';
// 为草稿内容定义一个清晰的 key
const LOCAL_STORAGE_DRAFT_KEY = 'writeDraftContent';
interface TemplateItem { interface TemplateItem {
id: string; id: string;
@ -56,13 +59,11 @@ interface KnowledgeBaseItem {
type MarkedHeadingToken = Tokens.Heading; type MarkedHeadingToken = Tokens.Heading;
type MarkedParagraphToken = Tokens.Paragraph; type MarkedParagraphToken = Tokens.Paragraph;
type MarkedImageToken = Tokens.Image; // <-- 新增:定义图片 Token 类型 type MarkedImageToken = Tokens.Image;
type MarkedListItem = Tokens.ListItem; type MarkedListItem = Tokens.ListItem;
type MarkedListToken = Tokens.List; type MarkedListToken = Tokens.List;
// 定义插入点标记以便在onChange时识别并移除 const INSERTION_MARKER = '';
// const INSERTION_MARKER = '【AI内容将插入此处】';
const INSERTION_MARKER = ''; // 改成空字符串,发现使用插入点标记体验不佳;
const Write = () => { const Write = () => {
const { t } = useTranslate('write'); const { t } = useTranslate('write');
@ -70,11 +71,9 @@ const Write = () => {
const [aiQuestion, setAiQuestion] = useState(''); const [aiQuestion, setAiQuestion] = useState('');
const [isAiLoading, setIsAiLoading] = useState(false); const [isAiLoading, setIsAiLoading] = useState(false);
const [dialogId] = useState(''); const [dialogId] = useState('');
// cursorPosition 存储用户点击设定的插入点位置
const [cursorPosition, setCursorPosition] = useState<number | null>(null); const [cursorPosition, setCursorPosition] = useState<number | null>(null);
// showCursorIndicator 现在仅用于控制文档中是否显示 'INSERTION_MARKER'
const [showCursorIndicator, setShowCursorIndicator] = useState(false); const [showCursorIndicator, setShowCursorIndicator] = useState(false);
const textAreaRef = useRef<any>(null); // Ant Design Input.TextArea 的 ref 类型 const textAreaRef = useRef<any>(null);
const [templates, setTemplates] = useState<TemplateItem[]>([]); const [templates, setTemplates] = useState<TemplateItem[]>([]);
const [isTemplateModalVisible, setIsTemplateModalVisible] = useState(false); const [isTemplateModalVisible, setIsTemplateModalVisible] = useState(false);
@ -93,20 +92,16 @@ const Write = () => {
const [modelTemperature, setModelTemperature] = useState<number>(1.0); const [modelTemperature, setModelTemperature] = useState<number>(1.0);
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBaseItem[]>([]); const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBaseItem[]>([]);
const [isLoadingKbs, setIsLoadingKbs] = useState(false); const [isLoadingKbs, setIsLoadingKbs] = useState(false);
const [isStreaming, setIsStreaming] = useState(false); // 标记AI是否正在流式输出 const [isStreaming, setIsStreaming] = useState(false);
// currentStreamedAiOutput 直接接收 useSendMessageWithSse 返回的累积内容
const [currentStreamedAiOutput, setCurrentStreamedAiOutput] = useState(''); const [currentStreamedAiOutput, setCurrentStreamedAiOutput] = useState('');
// 使用 useRef 存储 AI 插入点前后的内容,以及插入点位置,避免在流式更新中出现闭包陷阱
const contentBeforeAiInsertionRef = useRef(''); const contentBeforeAiInsertionRef = useRef('');
const contentAfterAiInsertionRef = useRef(''); const contentAfterAiInsertionRef = useRef('');
const aiInsertionStartPosRef = useRef<number | null>(null); const aiInsertionStartPosRef = useRef<number | null>(null);
// 使用 useFetchKnowledgeList hook 获取真实数据
const { list: knowledgeList, loading: isLoadingKnowledgeList } = const { list: knowledgeList, loading: isLoadingKnowledgeList } =
useFetchKnowledgeList(true); useFetchKnowledgeList(true);
// 使用流式消息发送钩子
const { const {
send: sendMessage, send: sendMessage,
answer, answer,
@ -138,8 +133,12 @@ const Write = () => {
[t], [t],
); );
const loadOrInitializeTemplates = useCallback(() => { //初始化逻辑
useEffect(() => {
// 定义一个内部函数来处理初始化,避免在 useEffect 中直接使用 async
const initialize = () => {
try { try {
// 加载或初始化模板列表
const initialized = localStorage.getItem(LOCAL_STORAGE_INIT_FLAG_KEY); const initialized = localStorage.getItem(LOCAL_STORAGE_INIT_FLAG_KEY);
let currentTemplates: TemplateItem[] = []; let currentTemplates: TemplateItem[] = [];
if (initialized === 'true') { if (initialized === 'true') {
@ -149,12 +148,6 @@ const Write = () => {
currentTemplates = savedTemplatesString currentTemplates = savedTemplatesString
? JSON.parse(savedTemplatesString) ? JSON.parse(savedTemplatesString)
: getInitialDefaultTemplateDefinitions(); : getInitialDefaultTemplateDefinitions();
if (!savedTemplatesString) {
localStorage.setItem(
LOCAL_STORAGE_TEMPLATES_KEY,
JSON.stringify(currentTemplates),
);
}
} else { } else {
currentTemplates = getInitialDefaultTemplateDefinitions(); currentTemplates = getInitialDefaultTemplateDefinitions();
localStorage.setItem( localStorage.setItem(
@ -164,25 +157,25 @@ const Write = () => {
localStorage.setItem(LOCAL_STORAGE_INIT_FLAG_KEY, 'true'); localStorage.setItem(LOCAL_STORAGE_INIT_FLAG_KEY, 'true');
} }
setTemplates(currentTemplates); setTemplates(currentTemplates);
if (currentTemplates.length > 0 && !selectedTemplate) {
setSelectedTemplate(currentTemplates[0].id); // 设定编辑器初始内容
setContent(currentTemplates[0].content); // 优先级 1: 尝试从 localStorage 加载上次的草稿
} else if (selectedTemplate) { const draftContent = localStorage.getItem(LOCAL_STORAGE_DRAFT_KEY);
const current = currentTemplates.find(
(ts) => ts.id === selectedTemplate, if (draftContent !== null) {
); // 如果存在草稿,无论模板是什么,都优先恢复草稿
if (current) setContent(current.content); setContent(draftContent);
else if (currentTemplates.length > 0) { } else if (currentTemplates.length > 0) {
setSelectedTemplate(currentTemplates[0].id); // 优先级 2: 如果没有草稿,则加载第一个模板作为初始内容
setContent(currentTemplates[0].content); const initialTemplate = currentTemplates[0];
} else { setSelectedTemplate(initialTemplate.id);
setSelectedTemplate(''); setContent(initialTemplate.content);
setContent('');
}
} }
// 如果既没有草稿也没有模板,内容将保持为空字符串
} catch (error) { } catch (error) {
console.error('加载或初始化模板失败:', error); console.error('加载或初始化模板与内容失败:', error);
message.error(t('loadTemplatesFailedError')); message.error(t('loadTemplatesFailedError'));
// 出现错误时的回退逻辑
const fallbackDefaults = getInitialDefaultTemplateDefinitions(); const fallbackDefaults = getInitialDefaultTemplateDefinitions();
setTemplates(fallbackDefaults); setTemplates(fallbackDefaults);
if (fallbackDefaults.length > 0) { if (fallbackDefaults.length > 0) {
@ -190,11 +183,25 @@ const Write = () => {
setContent(fallbackDefaults[0].content); setContent(fallbackDefaults[0].content);
} }
} }
}, [selectedTemplate, getInitialDefaultTemplateDefinitions, t]); };
initialize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // <-- 使用空依赖数组,确保此 effect 只在组件首次挂载时运行一次!
// 在 content 变化时,自动保存草稿到 localStorage
useEffect(() => { useEffect(() => {
loadOrInitializeTemplates(); // 使用防抖技术,避免过于频繁地写入 localStorage
}, [loadOrInitializeTemplates]); const timer = setTimeout(() => {
// 只有在 content 不是初始空状态时才保存,避免覆盖有意义的草稿为空内容
if (content) {
localStorage.setItem(LOCAL_STORAGE_DRAFT_KEY, content);
}
}, 1000); // 延迟1秒保存
// 组件卸载或 content 更新时,清除上一个计时器
return () => clearTimeout(timer);
}, [content]);
// 将 knowledgeList 数据同步到 knowledgeBases 状态 // 将 knowledgeList 数据同步到 knowledgeBases 状态
useEffect(() => { useEffect(() => {
@ -210,31 +217,26 @@ const Write = () => {
}, [knowledgeList, isLoadingKnowledgeList]); }, [knowledgeList, isLoadingKnowledgeList]);
// --- 调整流式响应处理逻辑 --- // --- 调整流式响应处理逻辑 ---
// 阶段1: 累积 AI 输出片段,用于实时显示(包括 <think> 标签)
useEffect(() => { useEffect(() => {
if (isStreaming && answer && answer.answer) { if (isStreaming && answer && answer.answer) {
setCurrentStreamedAiOutput(answer.answer); setCurrentStreamedAiOutput(answer.answer);
} }
}, [isStreaming, answer]); }, [isStreaming, answer]);
// 阶段2: 当流式输出完成时 (done 为 true)
useEffect(() => { useEffect(() => {
if (done) { if (done) {
setIsStreaming(false); setIsStreaming(false);
setIsAiLoading(false); setIsAiLoading(false);
let processedAiOutput = currentStreamedAiOutput; let processedAiOutput = currentStreamedAiOutput;
if (processedAiOutput) { if (processedAiOutput) {
// Regex to remove <think>...</think> including content
processedAiOutput = processedAiOutput.replace( processedAiOutput = processedAiOutput.replace(
/<think>.*?<\/think>/gs, /<think>.*?<\/think>/gs,
'', '',
); );
} }
// 将最终累积的AI内容和初始文档内容拼接更新到主内容状态
setContent((prevContent) => { setContent((prevContent) => {
if (aiInsertionStartPosRef.current !== null) { if (aiInsertionStartPosRef.current !== null) {
// 使用 useRef 中存储的初始内容和最终处理过的 AI 输出
const finalContent = const finalContent =
contentBeforeAiInsertionRef.current + contentBeforeAiInsertionRef.current +
processedAiOutput + processedAiOutput +
@ -244,7 +246,6 @@ const Write = () => {
return prevContent; return prevContent;
}); });
// AI完成回答后将光标实际移到新内容末尾
if ( if (
textAreaRef.current?.resizableTextArea?.textArea && textAreaRef.current?.resizableTextArea?.textArea &&
aiInsertionStartPosRef.current !== null aiInsertionStartPosRef.current !== null
@ -259,65 +260,34 @@ const Write = () => {
setCursorPosition(newCursorPos); setCursorPosition(newCursorPos);
} }
// 清理流式相关的临时状态和 useRef setCurrentStreamedAiOutput('');
setCurrentStreamedAiOutput(''); // 清空累积内容
contentBeforeAiInsertionRef.current = ''; contentBeforeAiInsertionRef.current = '';
contentAfterAiInsertionRef.current = ''; contentAfterAiInsertionRef.current = '';
aiInsertionStartPosRef.current = null; aiInsertionStartPosRef.current = null;
setShowCursorIndicator(true); setShowCursorIndicator(true);
} }
}, [done, currentStreamedAiOutput]); // 依赖 done 和 currentStreamedAiOutput确保在 done 时拿到最新的 currentStreamedAiOutput }, [done, currentStreamedAiOutput]);
// 监听 currentStreamedAiOutput 的变化,实时更新主 content 状态以实现流式显示
useEffect(() => { useEffect(() => {
if (isStreaming && aiInsertionStartPosRef.current !== null) { if (isStreaming && aiInsertionStartPosRef.current !== null) {
// 实时更新编辑器内容,保留 <think> 标签内容
setContent( setContent(
contentBeforeAiInsertionRef.current + contentBeforeAiInsertionRef.current +
currentStreamedAiOutput + currentStreamedAiOutput +
contentAfterAiInsertionRef.current, contentAfterAiInsertionRef.current,
); );
// 同时更新 cursorPosition让光标跟随 AI 输出移动(基于包含 think 标签的原始长度)
setCursorPosition( setCursorPosition(
aiInsertionStartPosRef.current + currentStreamedAiOutput.length, aiInsertionStartPosRef.current + currentStreamedAiOutput.length,
); );
} }
}, [currentStreamedAiOutput, isStreaming, aiInsertionStartPosRef]); }, [currentStreamedAiOutput, isStreaming]); // 移除了 aiInsertionStartPosRef 的依赖
useEffect(() => {
const loadDraftContent = () => {
try {
const draftContent = localStorage.getItem('writeDraftContent');
if (
draftContent &&
!content &&
(!selectedTemplate ||
templates.find((t) => t.id === selectedTemplate)?.content === '')
) {
setContent(draftContent);
}
} catch (error) {
console.error('加载暂存内容失败:', error);
}
};
if (localStorage.getItem(LOCAL_STORAGE_INIT_FLAG_KEY) === 'true') {
loadDraftContent();
}
}, [content, selectedTemplate, templates]);
useEffect(() => {
// 防抖保存,防止频繁写入 localStorage
const timer = setTimeout(
() => localStorage.setItem('writeDraftContent', content),
1000,
);
return () => clearTimeout(timer);
}, [content]);
// 当用户主动选择一个模板时,用模板内容覆盖当前编辑器内容
const handleTemplateSelect = (templateId: string) => { const handleTemplateSelect = (templateId: string) => {
setSelectedTemplate(templateId); setSelectedTemplate(templateId);
const item = templates.find((t) => t.id === templateId); const item = templates.find((t) => t.id === templateId);
if (item) setContent(item.content); if (item) {
setContent(item.content); // 这会覆盖当前内容,并触发上面的 useEffect 保存为新的草稿
}
}; };
const handleSaveTemplate = () => { const handleSaveTemplate = () => {
@ -352,7 +322,6 @@ const Write = () => {
} }
}; };
// 删除模板
const handleDeleteTemplate = (templateId: string) => { const handleDeleteTemplate = (templateId: string) => {
try { try {
const updatedTemplates = templates.filter((t) => t.id !== templateId); const updatedTemplates = templates.filter((t) => t.id !== templateId);
@ -363,8 +332,9 @@ const Write = () => {
); );
if (selectedTemplate === templateId) { if (selectedTemplate === templateId) {
if (updatedTemplates.length > 0) { if (updatedTemplates.length > 0) {
setSelectedTemplate(updatedTemplates[0].id); const newSelectedTemplate = updatedTemplates[0];
setContent(updatedTemplates[0].content); setSelectedTemplate(newSelectedTemplate.id);
setContent(newSelectedTemplate.content);
} else { } else {
setSelectedTemplate(''); setSelectedTemplate('');
setContent(''); setContent('');
@ -377,26 +347,18 @@ const Write = () => {
} }
}; };
// 获取上下文内容的辅助函数
const getContextContent = ( const getContextContent = (
cursorPos: number, cursorPos: number,
currentDocumentContent: string, currentDocumentContent: string,
maxLength: number = 4000, // 截取插入点上下文总共4000个字符 maxLength: number = 4000,
) => { ) => {
// 注意: 这里的 currentDocumentContent 传入的是 AI 提问时编辑器里的总内容,
// 而不是 contentBeforeAiInsertionRef + contentAfterAiInsertionRef因为可能包含标记
const beforeCursor = currentDocumentContent.substring(0, cursorPos); const beforeCursor = currentDocumentContent.substring(0, cursorPos);
const afterCursor = currentDocumentContent.substring(cursorPos); const afterCursor = currentDocumentContent.substring(cursorPos);
// 使用更明显的插入点标记
// const insertMarker = '[AI 内容插入点]';
const insertMarker = ''; const insertMarker = '';
const availableLength = maxLength - insertMarker.length; const availableLength = maxLength - insertMarker.length;
if (currentDocumentContent.length <= availableLength) { if (currentDocumentContent.length <= availableLength) {
return { return {
beforeCursor,
afterCursor,
contextContent: beforeCursor + insertMarker + afterCursor, contextContent: beforeCursor + insertMarker + afterCursor,
}; };
} }
@ -405,25 +367,20 @@ const Write = () => {
let finalBefore = beforeCursor; let finalBefore = beforeCursor;
let finalAfter = afterCursor; let finalAfter = afterCursor;
// 如果前半部分太长,截断并在前面加省略号
if (beforeCursor.length > halfLength) { if (beforeCursor.length > halfLength) {
finalBefore = finalBefore =
'...' + beforeCursor.substring(beforeCursor.length - halfLength + 3); '...' + beforeCursor.substring(beforeCursor.length - halfLength + 3);
} }
// 如果后半部分太长,截断并在后面加省略号
if (afterCursor.length > halfLength) { if (afterCursor.length > halfLength) {
finalAfter = afterCursor.substring(0, halfLength - 3) + '...'; finalAfter = afterCursor.substring(0, halfLength - 3) + '...';
} }
return { return {
beforeCursor,
afterCursor,
contextContent: finalBefore + insertMarker + finalAfter, contextContent: finalBefore + insertMarker + finalAfter,
}; };
}; };
// 处理问题请求提交
const handleAiQuestionSubmit = async ( const handleAiQuestionSubmit = async (
e: React.KeyboardEvent<HTMLTextAreaElement>, e: React.KeyboardEvent<HTMLTextAreaElement>,
) => { ) => {
@ -434,18 +391,15 @@ const Write = () => {
return; return;
} }
// 检查是否选择了知识库
if (selectedKnowledgeBases.length === 0) { if (selectedKnowledgeBases.length === 0) {
message.warning('请至少选择一个知识库'); message.warning('请至少选择一个知识库');
return; return;
} }
// 如果AI正在流式输出停止它并处理新问题
if (isStreaming) { if (isStreaming) {
stopOutputMessage(); // 停止当前的流式输出 stopOutputMessage();
setIsStreaming(false); // 立即设置为false中断流 setIsStreaming(false);
setIsAiLoading(false); // 确保加载状态也停止 setIsAiLoading(false);
const contentToCleanOnInterrupt = const contentToCleanOnInterrupt =
contentBeforeAiInsertionRef.current + contentBeforeAiInsertionRef.current +
currentStreamedAiOutput + currentStreamedAiOutput +
@ -455,46 +409,39 @@ const Write = () => {
'', '',
); );
setContent(cleanedContent); setContent(cleanedContent);
setCurrentStreamedAiOutput('');
setCurrentStreamedAiOutput(''); // 清除旧的流式内容 contentBeforeAiInsertionRef.current = '';
contentBeforeAiInsertionRef.current = ''; // 清理 useRef
contentAfterAiInsertionRef.current = ''; contentAfterAiInsertionRef.current = '';
aiInsertionStartPosRef.current = null; aiInsertionStartPosRef.current = null;
message.info('已中断上一次AI回答正在处理新问题...'); message.info('已中断上一次AI回答正在处理新问题...');
// 稍作延迟,确保状态更新后再处理新问题,防止竞态条件
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 100); setTimeout(resolve, 100);
}); });
} }
// 如果当前光标位置无效,提醒用户设置
if (cursorPosition === null) { if (cursorPosition === null) {
message.warning('请先点击文本框以设置AI内容插入位置。'); message.warning('请先点击文本框以设置AI内容插入位置。');
return; return;
} }
// 捕获 AI 插入点前后的静态内容,存储到 useRef
const currentCursorPos = cursorPosition; const currentCursorPos = cursorPosition;
// 此时的 content 应该是用户当前编辑器的实际内容包括可能存在的INSERTION_MARKER
// 但由于 INSERTION_MARKER 为空,所以就是当前的主 content
contentBeforeAiInsertionRef.current = content.substring( contentBeforeAiInsertionRef.current = content.substring(
0, 0,
currentCursorPos, currentCursorPos,
); );
contentAfterAiInsertionRef.current = content.substring(currentCursorPos); contentAfterAiInsertionRef.current = content.substring(currentCursorPos);
aiInsertionStartPosRef.current = currentCursorPos; // 记录确切的开始插入位置 aiInsertionStartPosRef.current = currentCursorPos;
setIsAiLoading(true); setIsAiLoading(true);
setIsStreaming(true); // 标记AI开始流式输出 setIsStreaming(true);
setCurrentStreamedAiOutput(''); // 清空历史累积内容,为新的流做准备 setCurrentStreamedAiOutput('');
try { try {
const authorization = localStorage.getItem('Authorization'); const authorization = localStorage.getItem('Authorization');
if (!authorization) { if (!authorization) {
message.error(t('loginRequiredError')); message.error(t('loginRequiredError'));
setIsAiLoading(false); setIsAiLoading(false);
setIsStreaming(false); // 停止流式标记 setIsStreaming(false);
// 失败时也清理临时状态
setCurrentStreamedAiOutput(''); setCurrentStreamedAiOutput('');
contentBeforeAiInsertionRef.current = ''; contentBeforeAiInsertionRef.current = '';
contentAfterAiInsertionRef.current = ''; contentAfterAiInsertionRef.current = '';
@ -502,12 +449,8 @@ const Write = () => {
return; return;
} }
// 构建请求内容将上下文内容发送给AI
let questionWithContext = aiQuestion; let questionWithContext = aiQuestion;
// 只有当用户设置了插入位置时才包含上下文
if (aiInsertionStartPosRef.current !== null) { if (aiInsertionStartPosRef.current !== null) {
// 传递给 getContextContent 的 content 应该是当前编辑器完整的包含marker的
const { contextContent } = getContextContent( const { contextContent } = getContextContent(
aiInsertionStartPosRef.current, aiInsertionStartPosRef.current,
content, content,
@ -515,7 +458,6 @@ const Write = () => {
questionWithContext = `${aiQuestion}\n\n上下文内容\n${contextContent}`; questionWithContext = `${aiQuestion}\n\n上下文内容\n${contextContent}`;
} }
// 发送流式请求
await sendMessage({ await sendMessage({
question: questionWithContext, question: questionWithContext,
kb_ids: selectedKnowledgeBases, kb_ids: selectedKnowledgeBases,
@ -525,28 +467,24 @@ const Write = () => {
temperature: modelTemperature, temperature: modelTemperature,
}); });
setAiQuestion(''); // 清空输入框 setAiQuestion('');
// 重新聚焦文本框但不是AI问答框而是主编辑区
if (textAreaRef.current?.resizableTextArea?.textArea) { if (textAreaRef.current?.resizableTextArea?.textArea) {
textAreaRef.current.resizableTextArea.textArea.focus(); textAreaRef.current.resizableTextArea.textArea.focus();
} }
} catch (error: any) { } catch (error: any) {
console.error('AI助手处理失败:', error); console.error('AI助手处理失败:', error);
if (error.code === 'ECONNABORTED' || error.name === 'AbortError') { const errorMessage =
message.error(t('aiRequestTimeoutError')); error.code === 'ECONNABORTED' || error.name === 'AbortError'
} else if (error.response?.data?.message) { ? t('aiRequestTimeoutError')
message.error( : error.response?.data?.message
`${t('aiRequestFailedError')}: ${error.response.data.message}`, ? `${t('aiRequestFailedError')}: ${error.response.data.message}`
); : t('aiRequestFailedError');
} else { message.error(errorMessage);
message.error(t('aiRequestFailedError'));
}
} }
} }
}; };
const handleSave = async () => { const handleSave = async () => {
// 将函数声明为 async
const selectedTemplateItem = templates.find( const selectedTemplateItem = templates.find(
(item) => item.id === selectedTemplate, (item) => item.id === selectedTemplate,
); );
@ -827,7 +765,6 @@ const Write = () => {
} }
}; };
// 修改编辑器渲染函数,添加光标标记
const renderEditor = () => { const renderEditor = () => {
let displayContent = content; // 默认显示主内容状态 let displayContent = content; // 默认显示主内容状态
@ -944,6 +881,7 @@ const Write = () => {
</div> </div>
); );
}; };
const renderPreview = () => ( const renderPreview = () => (
<div <div
style={{ style={{
@ -995,6 +933,7 @@ const Write = () => {
} }
}; };
// ... JSX 结构无变化,保持原样
return ( return (
<Layout <Layout
style={{ style={{
@ -1055,6 +994,7 @@ const Write = () => {
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
actions={[ actions={[
item.isCustom && ( // 只对自定义模板显示删除按钮
<Popconfirm <Popconfirm
key="delete" key="delete"
title={t('confirmDeleteTemplate')} title={t('confirmDeleteTemplate')}
@ -1068,7 +1008,8 @@ const Write = () => {
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
size="small" size="small"
/> />
</Popconfirm>, </Popconfirm>
),
]} ]}
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
@ -1275,13 +1216,11 @@ const Write = () => {
onKeyDown={handleAiQuestionSubmit} onKeyDown={handleAiQuestionSubmit}
disabled={isAiLoading} disabled={isAiLoading}
/> />
{isStreaming ? (
{/* 插入位置提示 或 AI正在回答时的提示 - 现已常驻显示 */}
{isStreaming ? ( // AI正在回答时优先显示此提示
<div <div
style={{ style={{
fontSize: '12px', fontSize: '12px',
color: '#faad14', // 警告色 color: '#faad14',
padding: '6px 10px', padding: '6px 10px',
backgroundColor: '#fffbe6', backgroundColor: '#fffbe6',
borderRadius: '4px', borderRadius: '4px',
@ -1290,8 +1229,7 @@ const Write = () => {
> >
AI正在生成回答... AI正在生成回答...
</div> </div>
) : // AI未回答时 ) : cursorPosition !== null ? (
cursorPosition !== null ? ( // 如果光标已设置
<div <div
style={{ style={{
fontSize: '12px', fontSize: '12px',
@ -1305,11 +1243,10 @@ const Write = () => {
💡 AI回答将插入到文档光标位置 ( {cursorPosition} ) 💡 AI回答将插入到文档光标位置 ( {cursorPosition} )
</div> </div>
) : ( ) : (
// 如果光标未设置
<div <div
style={{ style={{
fontSize: '12px', fontSize: '12px',
color: '#f5222d', // 错误色,提醒用户 color: '#f5222d',
padding: '6px 10px', padding: '6px 10px',
backgroundColor: '#fff1f0', backgroundColor: '#fff1f0',
borderRadius: '4px', borderRadius: '4px',