标签内容
+ displayContent =
+ contentBeforeAiInsertionRef.current +
+ currentStreamedAiOutput +
+ contentAfterAiInsertionRef.current;
+ } else if (showCursorIndicator && cursorPosition !== null) {
+ // 如果不处于流式输出中,但设置了光标,则显示插入标记
+ // (由于 INSERTION_MARKER 为空字符串,这一步实际上不会添加可见标记)
+ const beforeCursor = content.substring(0, cursorPosition);
+ const afterCursor = content.substring(cursorPosition);
+ displayContent = beforeCursor + INSERTION_MARKER + afterCursor;
+ }
+
+ return (
+
+ {
+ const currentInputValue = e.target.value; // 获取当前输入框中的完整内容
+ const newCursorSelectionStart = e.target.selectionStart;
+ let finalContent = currentInputValue;
+ let finalCursorPosition = newCursorSelectionStart;
+
+ // 如果用户在 AI 流式输出时输入,则中断 AI 输出,并“固化”当前内容(清除 标签)
+ if (isStreaming) {
+ stopOutputMessage(); // 中断 SSE 连接
+ setIsStreaming(false); // 停止流式输出
+ setIsAiLoading(false); // 停止加载状态
+
+ // 此时 currentInputValue 已经包含了所有已流出的 AI 内容 (包括 标签)
+ // 移除 标签
+ const contentWithoutThinkTags = currentInputValue.replace(
+ /.*?<\/think>/gs,
+ '',
+ );
+ finalContent = contentWithoutThinkTags;
+
+ // 重新计算光标位置,因为内容长度可能因移除 标签而改变
+ const originalLength = currentInputValue.length;
+ const cleanedLength = finalContent.length;
+
+ // 假设光标是在 AI 插入点之后,或者在用户输入后新位置,需要调整
+ // 如果光标在被移除的 区域内部,或者在移除区域之后,需要回退相应长度
+ if (
+ newCursorSelectionStart > (aiInsertionStartPosRef.current || 0)
+ ) {
+ // 假设 aiInsertionStartPosRef.current 是 AI 内容的起始点
+ finalCursorPosition =
+ newCursorSelectionStart - (originalLength - cleanedLength);
+ // 确保光标不会超出新内容的末尾
+ if (finalCursorPosition > cleanedLength) {
+ finalCursorPosition = cleanedLength;
+ }
+ } else {
+ finalCursorPosition = newCursorSelectionStart; // 光标在 AI 插入点之前,无需调整
+ }
+
+ // 清理流式相关的临时状态和 useRef
+ setCurrentStreamedAiOutput('');
+ contentBeforeAiInsertionRef.current = '';
+ contentAfterAiInsertionRef.current = '';
+ aiInsertionStartPosRef.current = null;
+ }
+
+ // 检查内容中是否包含 INSERTION_MARKER,如果包含则移除
+ // 由于 INSERTION_MARKER 为空字符串,此逻辑块影响很小
+ const markerIndex = finalContent.indexOf(INSERTION_MARKER); // 对已处理的 finalContent 进行检查
+ if (markerIndex !== -1) {
+ const contentWithoutMarker = finalContent.replace(
+ INSERTION_MARKER,
+ '',
+ );
+ finalContent = contentWithoutMarker;
+ if (newCursorSelectionStart > markerIndex) {
+ // 此处的 newCursorSelectionStart 仍然是原始的,需要与 markerIndex 比较
+ finalCursorPosition =
+ finalCursorPosition - INSERTION_MARKER.length;
+ }
+ }
+
+ setContent(finalContent); // 更新主内容状态
+ setCursorPosition(finalCursorPosition); // 更新光标位置状态
+ // 手动设置光标位置
+ // 这里不能直接操作 DOM,因为是在 setState 之后,DOM 尚未更新
+ // Ant Design Input.TextArea 会在 value 更新后自动处理光标位置
+ setShowCursorIndicator(true); // 用户输入时,表明已设置光标位置,持续显示标记
+ }}
+ onClick={(e) => {
+ const target = e.target as HTMLTextAreaElement;
+ setCursorPosition(target.selectionStart);
+ setShowCursorIndicator(true); // 点击时设置光标位置并显示标记
+ target.focus(); // 确保点击后立即聚焦
+ }}
+ onKeyUp={(e) => {
+ const target = e.target as HTMLTextAreaElement;
+ setCursorPosition(target.selectionStart);
+ setShowCursorIndicator(true); // 键盘抬起时设置光标位置并显示标记
+ }}
+ placeholder={t('writePlaceholder')}
+ autoSize={false}
+ />
+
+ );
+ };
const renderPreview = () => (
{
}}
>
+ {/* 预览模式下,通常不显示 标签,所以这里不需要特殊处理 */}
{content || t('previewPlaceholder')}
@@ -874,11 +1198,6 @@ const Write = () => {
}}
style={{ flexShrink: 0 }}
>
- {isAiLoading && (
-
- {t('aiLoadingMessage')}...
-
- )}
{
onKeyDown={handleAiQuestionSubmit}
disabled={isAiLoading}
/>
+
+ {/* 插入位置提示 或 AI正在回答时的提示 - 现已常驻显示 */}
+ {isStreaming ? ( // AI正在回答时优先显示此提示
+
+ ✨ AI正在生成回答,请稍候...
+
+ ) : // AI未回答时
+ cursorPosition !== null ? ( // 如果光标已设置
+
+ 💡 AI回答将插入到文档光标位置 (第 {cursorPosition} 个字符)。
+
+ ) : (
+ // 如果光标未设置
+
+ 👆 请在上方文档中点击,设置AI内容插入位置。
+
+ )}