2886 lines
116 KiB
TypeScript
2886 lines
116 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
||
import {
|
||
GitCompare,
|
||
MessageSquare,
|
||
CheckCircle,
|
||
AlertCircle,
|
||
ArrowDown,
|
||
ExternalLink,
|
||
Square,
|
||
X,
|
||
BookOpen,
|
||
User2Icon,
|
||
} from 'lucide-react';
|
||
import { Dialog, Select, Slider } from 'tdesign-react';
|
||
import { useTheme } from '../../contexts/ThemeContext';
|
||
import { PageType } from '../../components/Sidebar';
|
||
import { ChatMessage, EmptyChatState, QAAvatar, QAComposer, QASingleRagTrace, QAThinkingRound, QAThinkingTrace, RetrievalThinkingTrace, SingleRagAnswer, TypingIndicator } from './QACommon';
|
||
import GeneralQAHeader from './GeneralQAHeader';
|
||
import GeneralQAConfigSidebar from './GeneralQAConfigSidebar';
|
||
import type { ComparePane, CompareResult, KnowledgeBaseChunk, Message, QAMode, qaModeItem, SourceItem, stateItem, ToolCallData, ViewMode } from './GeneralQA.types';
|
||
import {
|
||
chatDataJSON,
|
||
createComparePane,
|
||
createMockSingleRagMessages,
|
||
createMultiRoundStreamTrace,
|
||
createThinkingTrace,
|
||
extractJsonObjects,
|
||
mapDocsToSources,
|
||
mapReferenceToSource,
|
||
parseSingleRagStream,
|
||
platformTypeChange,
|
||
} from './GeneralQA.mock';
|
||
import { requestCrudService } from '@/service/request-axios-service';
|
||
|
||
/**
|
||
* 通用问答组件
|
||
* 支持单轮RAG、多轮RAG、深度检索及结果对比功能
|
||
* @param onNavigate - 页面导航函数
|
||
* @param initialMode - 初始模式(可选)
|
||
*/
|
||
|
||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||
const chatUrl = import.meta.env.VITE_AIPI_CHAT_URL || '';
|
||
const generateUrl = import.meta.env.VITE_AIPI_GENERATE_CHAT_URL || '';
|
||
const taskChatUrl = import.meta.env.VITE_AIPI_TASK_CHAT_URL || '';
|
||
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
|
||
const service = requestCrudService;
|
||
const qaModeList = [
|
||
{ value: 'hx-search' as const, label: '画像检索', icon: User2Icon, desc: '检索相关账号' },
|
||
];
|
||
const ActionChat: React.FC<{ onNavigate: (page: PageType) => void; initialMode?: string }> = ({ onNavigate }) => {
|
||
const { theme } = useTheme();
|
||
const isDark = theme === 'dark';
|
||
|
||
// 状态管理
|
||
// 视图模式:单一模式或对比模式
|
||
const [viewMode, setViewMode] = useState<ViewMode>('single');
|
||
// 问答模式:单轮RAG、多轮RAG、深度检索
|
||
const [qaMode, setQaMode] = useState<QAMode>('single-round');
|
||
// 对比模式下的左侧模式
|
||
const [compareLeftMode, setCompareLeftMode] = useState<QAMode>('single-round');
|
||
// 对比模式下的右侧模式
|
||
const [compareRightMode, setCompareRightMode] = useState<QAMode>('multi-round');
|
||
|
||
// 知识范围设置
|
||
const [knowledgeScope, setKnowledgeScope] = useState<'all' | 'specific-kb' | 'specific-domain'>('all');
|
||
|
||
// 回答偏好设置
|
||
const [answerStyle, setAnswerStyle] = useState<'concise' | 'detailed'>('detailed');
|
||
const [showSources, setShowSources] = useState(true);
|
||
const [showProcess, setShowProcess] = useState(false);
|
||
|
||
// 对比设置
|
||
const [compareLayout, setCompareLayout] = useState<'horizontal' | 'vertical'>('horizontal');
|
||
const [autoSummarizeDiff, setAutoSummarizeDiff] = useState(true);
|
||
const [recommendBetter, setRecommendBetter] = useState(true);
|
||
|
||
// 消息和结果状态
|
||
// `messages` 驱动单一模式聊天区;`compareResult` 驱动对比模式左右面板。
|
||
// `isPreConnectLoading` 表示请求已发起但还没拿到可展示的 assistant 消息,用于显示“正在连接/生成”的占位状态。
|
||
const [messages, setMessages] = useState<Message[]>([]);
|
||
// 用户当前输入的文本内容,用于绑定输入框
|
||
const [inputValue, setInputValue] = useState('');
|
||
// 是否正在处理请求(包含真实请求和模拟请求)
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
// 是否处于"连接中"状态,表示已发起请求但还未收到可展示的响应
|
||
const [isPreConnectLoading, setIsPreConnectLoading] = useState(false);
|
||
// 存储发生的错误信息,用于显示错误提示
|
||
const [error, setError] = useState<string | null>(null);
|
||
// 对比模式下的结果数据,包含左右两侧的回答和来源信息
|
||
const [compareResult, setCompareResult] = useState<CompareResult[]>([]);
|
||
// 对比模式下用户输入的问题内容,用于左右对比时的统一输入
|
||
const [compareUserInput, setCompareUserInput] = useState('');
|
||
// 是否折叠左侧配置面板,true 为折叠,false 为展开
|
||
const [isLeftSidebarCollapsed, setIsLeftSidebarCollapsed] = useState(false);
|
||
// 是否启用自动滚动,当用户手动滚动时会自动关闭
|
||
const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true);
|
||
// 是否显示"滚动到底部"按钮,当有未读消息且用户已滚动时显示
|
||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
||
// 当前选中的引用来源,用于显示引用详情弹窗
|
||
const [selectedCitation, setSelectedCitation] = useState<SourceItem | null>(null);
|
||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
||
const [editingThinking, setEditingThinking] = useState('');
|
||
const [editingAnswer, setEditingAnswer] = useState('');
|
||
// 模型参数设置弹窗状态。
|
||
// 这些值会进入单轮 RAG 的真实请求体;模拟流仍复用同一套 UI,方便接口恢复后无缝切换。
|
||
const [isModelSettingsOpen, setIsModelSettingsOpen] = useState(false);
|
||
const [modelName, setModelName] = useState('OpenAI3');
|
||
const [scoreThreshold, setScoreThreshold] = useState(0.5);
|
||
const [temperature, setTemperature] = useState(0.3);
|
||
|
||
// 当模型正在生成时,如果用户切换模式或跳转页面,先把原动作暂存到 ref,待用户确认停止后再执行。
|
||
// 使用 ref 而不是 state 可以避免弹窗确认过程触发额外渲染,也不会被闭包中的旧状态影响。
|
||
const pendingActionRef = useRef<(() => void) | null>(null);
|
||
|
||
// 防止 Enter keydown 和按钮 click 在 React 状态更新前重复触发提交。
|
||
const submitLockRef = useRef(false);
|
||
|
||
// 真实流式请求的 reader 句柄,用于“停止生成”时取消浏览器流读取。
|
||
const readerRef = useRef<any>(null);
|
||
// 对比模式会同时启动左右两路请求,因此分别保存 reader,保证可以单独停止某一侧生成。
|
||
const compareReaderRef = useRef<{
|
||
left: any;
|
||
right: any;
|
||
}>({ left: null, right: null });
|
||
// 多轮接口创建任务后会返回 task_id,这里预留给后续“按任务取消/查询状态”等能力。
|
||
const taskIdRef = useRef<string | null>(null);
|
||
|
||
// 本地模拟流没有真实 reader,因此用停止标记在异步循环中主动中断。
|
||
const mockSingleRoundStoppedRef = useRef(false);
|
||
const mockMultiRoundStoppedRef = useRef(false);
|
||
const chatListRef = useRef<HTMLDivElement | null>(null);
|
||
const userId = useRef(387); // 用户id
|
||
const qaType = useRef('ACTION'); // 问答类型
|
||
const chatDataId = useRef(); // 聊天窗口id
|
||
const compareChatId = useRef(); // 对比对话记录id
|
||
|
||
// 检索过程展示状态
|
||
const [searchProcess, setSearchProcess] = useState({
|
||
currentMode: '单轮RAG',
|
||
rounds: 1,
|
||
hits: 5,
|
||
queryRewrite: '',
|
||
});
|
||
|
||
const state = useRef<stateItem>({
|
||
compareIndex: 0,
|
||
compareId: '',
|
||
messageList: [],
|
||
compareResult: []
|
||
})
|
||
|
||
/**
|
||
* 处理单轮 RAG 问答请求
|
||
*
|
||
* 单轮 RAG 的处理流程:
|
||
* 1. 向知识库问答 API 发送 POST 请求,携带用户问题和检索参数
|
||
* 2. API 返回 SSE 流式响应,包含多个 JSON 对象
|
||
* 3. 逐个解析流式 JSON,提取 answer 和 docs 字段
|
||
* 4. 通过 applyAnswerToken 将流式 token 追加到消息的 thinkingMarkdown/answerMarkdown
|
||
* 5. 实时更新消息来源(sources)和检索统计(searchProcess)
|
||
*
|
||
* @param query - 用户输入的问题文本
|
||
*/
|
||
const handleSingleRoundRun = async (query: string, side: 'left' | 'right' | null = null) => {
|
||
// 清空之前的错误信息
|
||
setError(null);
|
||
// 标记开始加载,禁用提交按钮防止重复点击
|
||
setIsLoading(true);
|
||
// 标记"连接中"状态,显示加载占位符
|
||
setIsPreConnectLoading(true);
|
||
// 用于存储当前 assistant 消息的 ID,以便后续更新该消息内容
|
||
let assistantId = '';
|
||
|
||
// 标志位:当前处理的 token 是否处于 <think>... 标签内(解析思考过程)
|
||
let inThink = false;
|
||
// 标志位:是否已经遇到过 <think> 标签开始解析思考内容
|
||
let thinkStarted = false;
|
||
|
||
/**
|
||
* 将流式接收到的 token 应用到对应消息的 singleRagTrace 中
|
||
* 该函数会解析 token 中的 <think>... 标签,将思考过程和回答分开存储
|
||
* @param token - 从流中接收到的文本片段
|
||
*/
|
||
const applyAnswerToken = (token: string) => {
|
||
const mesItem = state.current.messageList[1];
|
||
// 获取当前消息的思考和回答内容
|
||
let thinkingMarkdown = mesItem.singleRagTrace.thinkingMarkdown;
|
||
let answerMarkdown = mesItem.singleRagTrace.answerMarkdown;
|
||
let thinkingComplete = mesItem.singleRagTrace.thinkingComplete || false;
|
||
// 剩余未处理的 token 内容
|
||
let remaining = token;
|
||
console.log('remaining', remaining)
|
||
// 当前处于 <think> 标签内(解析思考过程),查找结束标签
|
||
while (remaining) {
|
||
if (inThink) {
|
||
const endIdx = remaining.indexOf('</think>');
|
||
if (endIdx === -1) {
|
||
thinkingMarkdown += remaining;
|
||
remaining = '';
|
||
} else {
|
||
thinkingMarkdown += remaining.slice(0, endIdx);
|
||
remaining = remaining.slice(endIdx + '</think>'.length);
|
||
inThink = false;
|
||
thinkingComplete = true;
|
||
}
|
||
} else {
|
||
const startIdx = remaining.indexOf('<think>');
|
||
console.log('startIdx', startIdx)
|
||
if (startIdx === -1) {
|
||
answerMarkdown += remaining;
|
||
remaining = '';
|
||
} else {
|
||
answerMarkdown += remaining.slice(0, startIdx);
|
||
remaining = remaining.slice(startIdx + '<think>'.length);
|
||
inThink = true;
|
||
thinkStarted = true;
|
||
}
|
||
}
|
||
}
|
||
state.current.messageList[1] = {
|
||
...mesItem,
|
||
singleRagTrace: {
|
||
...mesItem.singleRagTrace,
|
||
// 更新思考和回答的 markdown 内容
|
||
thinkingMarkdown,
|
||
answerMarkdown,
|
||
// 只有当已经开始解析思考内容时才更新 thinkingComplete 标志
|
||
thinkingComplete: thinkStarted ? thinkingComplete : true,
|
||
},
|
||
}
|
||
if (!side) {
|
||
setMessages((prev) => prev.map((msg) => {
|
||
// 跳过不是当前 assistant 消息或没有 singleRagTrace 的消息
|
||
if (msg.id !== assistantId || !msg.singleRagTrace) return msg;
|
||
// 返回更新后的消息对象,保留 singleRagTrace 中的所有字段
|
||
return {
|
||
...state.current.messageList[1],
|
||
};
|
||
}));
|
||
} else {
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
singleRagTrace: {
|
||
title: '大模型',
|
||
thinkingMarkdown,
|
||
answerMarkdown,
|
||
thinkingComplete: thinkStarted ? thinkingComplete : true,
|
||
},
|
||
};
|
||
});
|
||
}
|
||
};
|
||
|
||
const updatePane = (side: 'left' | 'right', updater: (pane: ComparePane) => ComparePane) => {
|
||
setCompareResult((prev) => {
|
||
if (!prev) return prev;
|
||
return {
|
||
...prev,
|
||
[side]: updater(prev[side]),
|
||
};
|
||
});
|
||
};
|
||
|
||
try {
|
||
// 向知识库问答 API 发送 POST 请求,携带用户问题和检索参数
|
||
const response = await fetch(`${chatUrl}/chat/knowledge_base_chat_tiaoshi`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
// 请求体包含查询内容、知识库名称、检索参数、模型配置等
|
||
body: JSON.stringify({
|
||
query, // 用户输入的问题
|
||
knowledge_base_name: [], // 知识库名称列表,空数组表示使用所有知识库
|
||
top_k: 5, // 返回最相似的 5 条结果
|
||
score_threshold: scoreThreshold, // 相似度阈值,过滤低质量检索结果
|
||
history: [], // 对话历史,用于多轮对话
|
||
stream: true, // 启用流式响应
|
||
model_name: modelName, // 使用的模型名称
|
||
temperature, // 生成温度,控制随机性
|
||
prompt_name: 'default', // 使用的提示词模板
|
||
local_doc_url: false, // 是否返回本地文档 URL
|
||
}),
|
||
});
|
||
|
||
// 检查 HTTP 响应状态
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
// 确保响应体存在(SSE 流需要 body)
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
// 收到有效响应后,关闭"连接中"状态,开始显示实际内容
|
||
setIsPreConnectLoading(false);
|
||
|
||
// 生成唯一 ID 用于标识这条 assistant 消息
|
||
assistantId = `assistant-${Date.now()}`;
|
||
// 初始化思考过程追踪对象
|
||
const initialTrace: QASingleRagTrace = {
|
||
title: '单轮 RAG 思考过程',
|
||
thinkingMarkdown: '', // 思考过程内容
|
||
answerMarkdown: '', // 最终回答内容
|
||
thinkingComplete: false, // 思考是否完成
|
||
};
|
||
const messageItem: Message = {
|
||
id: assistantId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
singleRagTrace: initialTrace,
|
||
sources: [], // 初始为空,随着检索结果更新
|
||
}
|
||
state.current.messageList.push(messageItem); // 数据存储
|
||
// 将 assistant 消息添加到消息列表中
|
||
setMessages((prev) => [...prev, messageItem]);
|
||
|
||
// 获取响应体的流式读取器
|
||
const reader = response.body.getReader();
|
||
// 保存 reader 引用,用于"停止生成"功能
|
||
if (!side) {
|
||
readerRef.current = reader;
|
||
} else {
|
||
compareReaderRef.current[side] = reader;
|
||
}
|
||
// 创建文本解码器,将二进制数据转换为字符串
|
||
const decoder = new TextDecoder();
|
||
// buffer 用于暂存未完成的 JSON 片段
|
||
let buffer = '';
|
||
let docsList: any[] = [];
|
||
try {
|
||
// 持续读取流数据,直到流结束
|
||
while (true) {
|
||
// 读取下一个数据块
|
||
const { done, value } = await reader.read();
|
||
// done 为 true 时表示流已结束
|
||
if (done) {
|
||
console.log('聊天记录', state.current.messageList);
|
||
chatHistoryAddSearch();
|
||
break;
|
||
}
|
||
|
||
// 将二进制数据解码并追加到 buffer
|
||
buffer += decoder.decode(value, { stream: true });
|
||
// 从 buffer 中提取完整的 JSON 对象
|
||
const extracted = extractJsonObjects(buffer);
|
||
if (buffer.includes('docs')) {
|
||
try {
|
||
docsList = chatDataJSON(buffer).docsList || [];
|
||
} catch (error: any) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
// 剩余无法解析的内容保留到下次处理
|
||
buffer = extracted.rest;
|
||
|
||
// 遍历所有提取到的 JSON 对象
|
||
for (const jsonText of extracted.objects) {
|
||
try {
|
||
// 解析知识库返回的 JSON 片段
|
||
const parsed = parseKnowledgeBaseChunk(jsonText);
|
||
// 如果有回答内容,追加到消息的 answerMarkdown
|
||
if (typeof parsed.answer === 'string') {
|
||
applyAnswerToken(parsed.answer);
|
||
}
|
||
// 如果有检索文档,更新消息的来源信息
|
||
if (parsed.docs) {
|
||
// 将 docs 转换为 sources 格式
|
||
const mappedDocs = docsList.map((item: any) => ({
|
||
title: item.m_title,
|
||
type: item.source1,
|
||
snippet: item.m_content,
|
||
source: item.source1,
|
||
time: item.m_publish,
|
||
url: item.recUuid,
|
||
content: item.m_content || '',
|
||
}));
|
||
// 更新检索统计信息
|
||
setSearchProcess((prev) => ({ ...prev, hits: prev.hits + mappedDocs.length }));
|
||
state.current.messageList[1].sources = mappedDocs;
|
||
// 更新消息的来源列表
|
||
if (!side) {
|
||
setMessages((prev) => prev.map((msg) => (
|
||
msg.id === assistantId ? { ...msg, sources: mappedDocs } : msg
|
||
)));
|
||
} else {
|
||
updatePane(side, (pane) => ({ ...pane, sources: mappedDocs }));
|
||
}
|
||
}
|
||
} catch (parseError) {
|
||
// 解析错误不影响主流程,记录日志继续处理
|
||
console.error('单轮RAG流式JSON解析错误:', parseError);
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
// 流读取完成后,释放 reader 锁并清空引用
|
||
if (readerRef.current) {
|
||
readerRef.current.releaseLock();
|
||
readerRef.current = null;
|
||
}
|
||
}
|
||
} catch (runError) {
|
||
// 请求失败处理
|
||
console.error('单轮RAG请求失败:', runError);
|
||
setError(runError instanceof Error ? runError.message : '发生未知错误,请稍后重试');
|
||
} finally {
|
||
// 无论成功还是失败,都要重置加载状态和解锁提交
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
state.current.messageList = [];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理单轮 RAG 问答请求
|
||
*
|
||
* 单轮 RAG 的处理流程:
|
||
* 1. 向知识库问答 API 发送 POST 请求,携带用户问题和检索参数
|
||
* 2. API 返回 SSE 流式响应,包含多个 JSON 对象
|
||
* 3. 逐个解析流式 JSON,提取 answer 和 docs 字段
|
||
* 4. 通过 applyAnswerToken 将流式 token 追加到消息的 thinkingMarkdown/answerMarkdown
|
||
* 5. 实时更新消息来源(sources)和检索统计(searchProcess)
|
||
*
|
||
* @param query - 用户输入的问题文本
|
||
*/
|
||
const chatRun = async (query: string, side: 'left' | 'right' | null = null) => {
|
||
// 清空之前的错误信息
|
||
setError(null);
|
||
// 标记开始加载,禁用提交按钮防止重复点击
|
||
setIsLoading(true);
|
||
// 标记"连接中"状态,显示加载占位符
|
||
setIsPreConnectLoading(true);
|
||
// 用于存储当前 assistant 消息的 ID,以便后续更新该消息内容
|
||
let assistantId = '';
|
||
|
||
// 标志位:当前处理的 token 是否处于 <think>... 标签内(解析思考过程)
|
||
let inThink = false;
|
||
// 标志位:是否已经遇到过 <think> 标签开始解析思考内容
|
||
let thinkStarted = false;
|
||
|
||
/**
|
||
* 将流式接收到的 token 应用到对应消息的 singleRagTrace 中
|
||
* 该函数会解析 token 中的 <think>... 标签,将思考过程和回答分开存储
|
||
* @param token - 从流中接收到的文本片段
|
||
*/
|
||
const applyAnswerToken = (token: string, side: 'left' | 'right' | null = null) => {
|
||
let mesItem: any = state.current.messageList[1];
|
||
if (side === 'left' || side === 'right') {
|
||
mesItem = state.current.compareResult[state.current.compareIndex][side];
|
||
}
|
||
// 获取当前消息的思考和回答内容
|
||
let thinkingMarkdown = mesItem.singleRagTrace.thinkingMarkdown;
|
||
let answerMarkdown = mesItem.singleRagTrace.answerMarkdown;
|
||
let thinkingComplete = mesItem.singleRagTrace.thinkingComplete || false;
|
||
// 剩余未处理的 token 内容
|
||
let remaining = token;
|
||
// console.log('remaining', remaining)
|
||
// 当前处于 <think> 标签内(解析思考过程),查找结束标签
|
||
while (remaining) {
|
||
if (inThink) {
|
||
const endIdx = remaining.indexOf('</think>');
|
||
if (endIdx === -1) {
|
||
thinkingMarkdown += remaining;
|
||
remaining = '';
|
||
} else {
|
||
thinkingMarkdown += remaining.slice(0, endIdx);
|
||
remaining = remaining.slice(endIdx + '</think>'.length);
|
||
inThink = false;
|
||
thinkingComplete = true;
|
||
}
|
||
} else {
|
||
const startIdx = remaining.indexOf('<think>');
|
||
if (startIdx === -1) {
|
||
answerMarkdown += remaining;
|
||
remaining = '';
|
||
} else {
|
||
answerMarkdown += remaining.slice(0, startIdx);
|
||
remaining = remaining.slice(startIdx + '<think>'.length);
|
||
inThink = true;
|
||
thinkStarted = true;
|
||
}
|
||
}
|
||
}
|
||
if (!side) {
|
||
state.current.messageList[1] = {
|
||
...mesItem,
|
||
singleRagTrace: {
|
||
...mesItem.singleRagTrace,
|
||
// 更新思考和回答的 markdown 内容
|
||
thinkingMarkdown,
|
||
answerMarkdown,
|
||
// 只有当已经开始解析思考内容时才更新 thinkingComplete 标志
|
||
thinkingComplete: thinkStarted ? thinkingComplete : true,
|
||
},
|
||
}
|
||
setMessages((prev) => prev.map((msg) => {
|
||
// 跳过不是当前 assistant 消息或没有 singleRagTrace 的消息
|
||
if (msg.id !== assistantId || !msg.singleRagTrace) return msg;
|
||
// 返回更新后的消息对象,保留 singleRagTrace 中的所有字段
|
||
return {
|
||
...state.current.messageList[1],
|
||
};
|
||
}));
|
||
} else {
|
||
state.current.compareResult[state.current.compareIndex][side].singleRagTrace = {
|
||
...mesItem.singleRagTrace,
|
||
// 更新思考和回答的 markdown 内容
|
||
thinkingMarkdown,
|
||
answerMarkdown,
|
||
// 只有当已经开始解析思考内容时才更新 thinkingComplete 标志
|
||
thinkingComplete: thinkStarted ? thinkingComplete : true,
|
||
}
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
singleRagTrace: {
|
||
title: '大模型',
|
||
thinkingMarkdown,
|
||
answerMarkdown,
|
||
thinkingComplete: thinkStarted ? thinkingComplete : true,
|
||
},
|
||
};
|
||
});
|
||
}
|
||
};
|
||
|
||
try {
|
||
// 向知识库问答 API 发送 POST 请求,携带用户问题和检索参数
|
||
const response = await fetch(`${chatUrl}/chat/chat`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
// 请求体包含查询内容、知识库名称、检索参数、模型配置等
|
||
body: JSON.stringify({
|
||
query, // 用户输入的问题
|
||
history: [], // 对话历史,用于多轮对话
|
||
stream: true, // 启用流式响应
|
||
model_name: modelName, // 使用的模型名称
|
||
temperature, // 生成温度,控制随机性
|
||
prompt_name: 'default', // 使用的提示词模板
|
||
max_tokens: 1000
|
||
}),
|
||
});
|
||
|
||
// 检查 HTTP 响应状态
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
// 确保响应体存在(SSE 流需要 body)
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
// 收到有效响应后,关闭"连接中"状态,开始显示实际内容
|
||
setIsPreConnectLoading(false);
|
||
|
||
// 生成唯一 ID 用于标识这条 assistant 消息
|
||
assistantId = `assistant-${Date.now()}`;
|
||
// 初始化思考过程追踪对象
|
||
const initialTrace: QASingleRagTrace = {
|
||
title: '大模型思考过程',
|
||
thinkingMarkdown: '', // 思考过程内容
|
||
answerMarkdown: '', // 最终回答内容
|
||
thinkingComplete: false, // 思考是否完成
|
||
};
|
||
const messageItem: Message = {
|
||
id: assistantId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
singleRagTrace: initialTrace,
|
||
sources: [], // 初始为空,随着检索结果更新
|
||
}
|
||
if (!side) {
|
||
state.current.messageList.push(messageItem);
|
||
// 将 assistant 消息添加到消息列表中
|
||
setMessages((prev) => [...prev, messageItem]);
|
||
} else if (side === 'left' || side === 'right') {
|
||
state.current.compareResult[state.current.compareIndex][side].singleRagTrace = initialTrace;
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
singleRagTrace: initialTrace,
|
||
};
|
||
});
|
||
}
|
||
|
||
// 获取响应体的流式读取器
|
||
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||
// 保存 reader 引用,用于"停止生成"功能
|
||
if (!side) {
|
||
readerRef.current = reader;
|
||
} else {
|
||
compareReaderRef.current[side] = reader;
|
||
}
|
||
// 创建文本解码器,将二进制数据转换为字符串
|
||
const decoder = new TextDecoder();
|
||
// buffer 用于暂存未完成的 JSON 片段
|
||
|
||
try {
|
||
// 持续读取流数据,直到流结束
|
||
while (true) {
|
||
// 读取下一个数据块
|
||
const { done, value } = await reader.read();
|
||
// done 为 true 时表示流已结束
|
||
if (done) {
|
||
console.log('聊天记录', state.current.messageList);
|
||
if (!side) {
|
||
chatHistoryAddSearch();
|
||
}
|
||
break;
|
||
}
|
||
applyAnswerToken(value, side);
|
||
}
|
||
} finally {
|
||
// 流读取完成后,释放 reader 锁并清空引用
|
||
if (readerRef.current) {
|
||
// readerRef.current.releaseLock();
|
||
readerRef.current = null;
|
||
}
|
||
}
|
||
} catch (runError) {
|
||
// 请求失败处理
|
||
console.error('单轮RAG请求失败:', runError);
|
||
setError(runError instanceof Error ? runError.message : '发生未知错误,请稍后重试');
|
||
} finally {
|
||
// 无论成功还是失败,都要重置加载状态和解锁提交
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
state.current.messageList = [];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 解析类 JavaScript 对象字符串中的字符串值。
|
||
*
|
||
* 后端单轮 RAG 流有时不是严格 JSON,而是 `{answer: '...'}` 这种对象片段。
|
||
* 该函数只负责把单/双引号包裹的 answer 内容还原成普通字符串,保留换行和转义字符。
|
||
*/
|
||
const parseLooseStringValue = (rawValue: string): string => {
|
||
try {
|
||
return JSON.parse(rawValue) as string;
|
||
} catch {
|
||
const trimmedValue = rawValue.trim();
|
||
if (trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) {
|
||
return trimmedValue
|
||
.slice(1, -1)
|
||
.replace(/\\n/g, '\n')
|
||
.replace(/\\r/g, '\r')
|
||
.replace(/\\t/g, '\t')
|
||
.replace(/\\'/g, "'")
|
||
.replace(/\\\\/g, '\\');
|
||
}
|
||
throw new Error(`无法解析字符串值: ${rawValue}`);
|
||
}
|
||
};
|
||
|
||
const parseLooseValue = (rawValue: string): unknown => {
|
||
try {
|
||
return JSON.parse(rawValue);
|
||
} catch {
|
||
return parseLooseStringValue(rawValue);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 将单轮知识库流中的一个对象片段转换为统一数据结构。
|
||
*
|
||
* 兼容两类返回:
|
||
* - 严格 JSON:`{"answer":"..."}` / `{"docs":[...]}`
|
||
* - 宽松对象:`{answer: '...'}` / `{docs: [...]}`
|
||
*
|
||
* 真实接口和本地模拟都走同一个解析函数,确保联调时只需要替换数据源,不需要改 UI 更新逻辑。
|
||
*/
|
||
const parseKnowledgeBaseChunk = (jsonText: string): KnowledgeBaseChunk => {
|
||
try {
|
||
return JSON.parse(jsonText) as KnowledgeBaseChunk;
|
||
} catch {
|
||
const objectText = jsonText.trim();
|
||
const answerMatch = objectText.match(/^\{\s*answer\s*:\s*([\s\S]*)\s*\}$/);
|
||
if (answerMatch) {
|
||
const rawValue = answerMatch[1].trim();
|
||
return { answer: parseLooseStringValue(rawValue) };
|
||
}
|
||
|
||
const docsMatch = objectText.match(/^\{\s*docs\s*:\s*([\s\S]*)\s*\}$/);
|
||
if (docsMatch) {
|
||
return { docs: parseLooseValue(docsMatch[1]) as KnowledgeBaseChunk['docs'] };
|
||
}
|
||
|
||
const normalizedText = objectText.replace(/([{,]\s*)(answer|docs)(\s*:)/g, '$1"$2"$3');
|
||
return JSON.parse(normalizedText) as KnowledgeBaseChunk;
|
||
}
|
||
};
|
||
|
||
const sleep = (ms: number) => new Promise((resolve) => {
|
||
window.setTimeout(resolve, ms);
|
||
});
|
||
const updatePane = (side: 'left' | 'right', updater: (pane: ComparePane) => ComparePane) => {
|
||
setCompareResult((prev) => prev.map((msg) => {
|
||
if (msg.id !== state.current.compareId) return msg
|
||
return {
|
||
...msg,
|
||
[side]: updater(msg[side]),
|
||
};
|
||
}));
|
||
};
|
||
/**
|
||
* 处理多轮真实接口里的工具调用事件。
|
||
*
|
||
* `show_text` 用于追加思考或最终回答;`local_search` / `tavily_search` 用于把检索结果填入最近一轮。
|
||
* 这里保持事件到 UI trace 的单向映射,避免请求层和展示层互相耦合。
|
||
*/
|
||
const handleToolCall = (toolData: ToolCallData, assistantId: string, side: 'left' | 'right' | null = null) => {
|
||
let mesItem: any = state.current.messageList[1];
|
||
if (side === 'left' || side === 'right') {
|
||
mesItem = state.current.compareResult[state.current.compareIndex][side];
|
||
console.log('mesItem', mesItem)
|
||
}
|
||
const trace = { ...mesItem.thinkingTrace, rounds: [...mesItem.thinkingTrace.rounds] };
|
||
const lastRound = trace.rounds[trace.rounds.length - 1];
|
||
switch (toolData.tool_name) {
|
||
case 'show_text': {
|
||
const chatText = toolData.tool_input.text || '';
|
||
if (chatText.includes('<think>')) {
|
||
const thinkContent = chatText.replace('<think>', '').replace('</think>', '').trim();
|
||
if (trace.rounds[0].title !== '正在准备检索') {
|
||
lastRound.queryLoading = false;
|
||
lastRound.queryShow = lastRound.retrievals.length > 0;
|
||
trace.rounds.push({
|
||
id: `round-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
title: '思考中',
|
||
thoughtMarkdown: thinkContent,
|
||
query: '',
|
||
queryLoading: true,
|
||
queryShow: true,
|
||
retrievals: [],
|
||
});
|
||
} else {
|
||
// lastRound.queryLoading = false;
|
||
trace.rounds[0].title = '思考过程'
|
||
trace.rounds[0].thoughtMarkdown = thinkContent;
|
||
}
|
||
} else {
|
||
lastRound.queryLoading = false;
|
||
trace.finalAnswerMarkdown += chatText;
|
||
trace.thinkingComplete = true;
|
||
}
|
||
break;
|
||
}
|
||
case 'tavily_search':
|
||
case 'local_search': {
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as {
|
||
query?: string;
|
||
results?: Array<{ title?: string; content?: string; url?: string }>;
|
||
};
|
||
|
||
lastRound.query = result.query || lastRound.query;
|
||
lastRound.queryLoading = false;
|
||
lastRound.title = toolData.tool_name === 'local_search' ? '本地知识检索' : '联网检索';
|
||
lastRound.retrievals = (result.results || []).map((item, idx) => ({
|
||
id: `${toolData.tool_name}-${idx}`,
|
||
title: item.title || '未命名结果',
|
||
content: item.content || '',
|
||
source: item.url || '',
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case 'google_search': { // 处理多轮检索
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as {
|
||
organic: Array<{ title?: string; content?: string; url?: string; recUuid?: string; publish_time?: string }>;
|
||
};
|
||
lastRound.queryLoading = false;
|
||
lastRound.title = '思考过程';
|
||
lastRound.retrievals = (result.organic || []).map((item, idx) => ({
|
||
id: item.recUuid,
|
||
title: item.title || '未命名结果',
|
||
content: item.content || '',
|
||
source: item.recUuid || '',
|
||
time: item.publish_time,
|
||
recUuid: item.recUuid,
|
||
type: 'more'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.q) {
|
||
lastRound.query = toolData.tool_input.q;
|
||
}
|
||
break;
|
||
}
|
||
case 'getTaskLikeTop': { // 任务检索
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as {
|
||
data: Array<{ taskName?: string; overview?: string; id?: number; keywords?: string; taskDirection?: string; taskLevel?: number; taskStartDate: string; taskEndDate: string }>;
|
||
}
|
||
console.log('result', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.title = '思考过程';
|
||
lastRound.retrievals = (result.data || []).map((item, idx) => ({
|
||
id: String(item.id),
|
||
title: item.taskName || '未命名结果',
|
||
content: item.overview || '',
|
||
keywords: item.keywords,
|
||
taskDirection: item.taskDirection,
|
||
time: `${item.taskStartDate}-${item.taskEndDate}`,
|
||
type: 'task'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword) {
|
||
lastRound.query = toolData.tool_input.keyword;
|
||
}
|
||
break;
|
||
}
|
||
case 'search_user_profile': { // 画像检索
|
||
lastRound.title = '靶标平台画像库';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ user_name?: string; user_id?: string; page_content?: string; 平台?: string; 类型?: string; 粉丝数?: number; }>;
|
||
console.log('result', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.user_id,
|
||
title: item.user_name || item.user_id || '-',
|
||
content: item.page_content || '',
|
||
platform: platformTypeChange(item.平台) || '',
|
||
fans: item.粉丝数 || 0,
|
||
type: 'hx'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query) {
|
||
lastRound.queryTitle = '检索词';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query || toolData.tool_input.user_fig;
|
||
}
|
||
break;
|
||
}
|
||
case 'get_taiwan_news': { // 查找台湾热点新闻
|
||
lastRound.title = '台湾热点新闻';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ user_name?: string; user_id?: string; content?: string; publish_time?: string; message_id?: string; platform?: string; comment_count?: number; }>
|
||
console.log('get_taiwan_news', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.user_id,
|
||
title: item.user_name || item.user_id || '-',
|
||
content: item.content || '',
|
||
platform: platformTypeChange(item.platform) || '',
|
||
comment_count: item.comment_count,
|
||
message_id: item.message_id,
|
||
time: item.publish_time,
|
||
type: 'taiwan_news'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query) {
|
||
lastRound.queryTitle = '搜索台湾新闻';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query;
|
||
}
|
||
break;
|
||
}
|
||
case 'search_keyword_taiwan_news': { // 查找台湾热点新闻
|
||
lastRound.title = '台湾热点新闻';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ user_name?: string; user_id?: string; content?: string; publish_time?: string; message_id?: string; platform?: string; comment_count?: number; }>
|
||
console.log('get_taiwan_news', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.user_id,
|
||
title: item.user_name || item.user_id || '-',
|
||
content: item.content || '',
|
||
platform: platformTypeChange(item.platform) || '',
|
||
comment_count: item.comment_count,
|
||
message_id: item.message_id,
|
||
time: item.publish_time,
|
||
type: 'taiwan_news'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query) {
|
||
lastRound.queryTitle = '搜索台湾新闻';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query;
|
||
}
|
||
break;
|
||
}
|
||
case 'get_world_news': {
|
||
lastRound.title = '世界热点新闻';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ user_name?: string; user_id?: string; content?: string; publish_time?: string; message_id?: string; platform?: string; comment_count?: number; }>
|
||
console.log('get_taiwan_news', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.user_id,
|
||
title: item.user_name || item.user_id || '-',
|
||
content: item.content || '',
|
||
platform: platformTypeChange(item.platform) || '',
|
||
comment_count: item.comment_count,
|
||
message_id: item.message_id,
|
||
time: item.publish_time,
|
||
type: 'world_news'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query) {
|
||
lastRound.queryTitle = '搜索世界新闻';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query;
|
||
}
|
||
break;
|
||
}
|
||
case 'get_root_messaage_comment': { // 获取某条消息的全部评论
|
||
lastRound.title = '获取该消息的全部评论';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ user_name?: string; user_id?: string; content?: string; publish_time?: string; message_id?: string; platform?: string; comment_count?: number; }>
|
||
console.log('get_taiwan_news', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.user_id,
|
||
title: item.user_name || item.user_id || '-',
|
||
content: item.content || '',
|
||
platform: platformTypeChange(item.platform) || '',
|
||
comment_count: item.comment_count,
|
||
message_id: item.message_id,
|
||
time: item.publish_time,
|
||
type: 'messaage_comment'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query || toolData.tool_input.platform) {
|
||
lastRound.queryTitle = '查找平台';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query || platformTypeChange(toolData.tool_input.platform);
|
||
}
|
||
break;
|
||
}
|
||
case 'search_user_behavior': { // 查询该用户的历史行为
|
||
lastRound.title = '查询该用户的历史行为';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result);
|
||
console.log('search_user_behavior', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result.slice(0, 10) || []).map((item, idx) => ({
|
||
id: item,
|
||
title: item,
|
||
content: item,
|
||
type: 'user_behavior'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query || toolData.tool_input.platform) {
|
||
lastRound.queryTitle = '查找平台';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query || platformTypeChange(toolData.tool_input.platform);
|
||
}
|
||
break;
|
||
}
|
||
case 'get_america_news': { // 根据关键词获取美国热点新闻
|
||
lastRound.title = '美国热点新闻';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ user_name?: string; user_id?: string; content?: string; publish_time?: string; message_id?: string; platform?: string; comment_count?: number; }>
|
||
console.log('get_taiwan_news', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.user_id,
|
||
title: item.user_name || item.user_id || '-',
|
||
content: item.content || '',
|
||
platform: platformTypeChange(item.platform) || '',
|
||
comment_count: item.comment_count,
|
||
message_id: item.message_id,
|
||
time: item.publish_time,
|
||
type: 'america_news'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query) {
|
||
lastRound.queryTitle = '查询美国热点新闻';
|
||
lastRound.query = toolData.tool_input.keyword || toolData.tool_input.query;
|
||
}
|
||
break;
|
||
}
|
||
case 'get_time_data': { // 态势新闻搜索
|
||
lastRound.title = '态势新闻搜索';
|
||
console.log('get_time_data', toolData.tool_input)
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result) as Array<{ title?: string; textid?: string; hot_num?: number; date?: string; content?: string }>
|
||
console.log('get_time_data', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.queryTitle = '查询态势新闻';
|
||
lastRound.retrievals = (result || []).map((item, idx) => ({
|
||
id: item.textid,
|
||
title: item.title,
|
||
content: item.content || '',
|
||
time: item.date,
|
||
hot_num: item.hot_num,
|
||
type: 'get_time_data'
|
||
}));
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
} else if (toolData.tool_input.keyword || toolData.tool_input.query || toolData.tool_input.s || toolData.tool_input.e) {
|
||
lastRound.query = `${toolData.tool_input.s} - ${toolData.tool_input.e}`;
|
||
}
|
||
break;
|
||
}
|
||
case 'get_textid_content': { // 获取新闻详细内容
|
||
lastRound.title = '获取新闻详细内容';
|
||
if (toolData.tool_input.result && lastRound) {
|
||
try {
|
||
const result = JSON.parse(toolData.tool_input.result)
|
||
console.log('get_textid_content', result);
|
||
lastRound.queryLoading = false;
|
||
lastRound.textidContent = result.content;
|
||
} catch (parseError) {
|
||
console.error('搜索结果JSON解析错误:', parseError);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
if (!side) {
|
||
state.current.messageList[1] = {
|
||
...mesItem,
|
||
thinkingTrace: trace,
|
||
}
|
||
setMessages((prevMessages) => prevMessages.map((msg) => {
|
||
if (msg.id !== assistantId || msg.role !== 'assistant' || !msg.thinkingTrace) return msg;
|
||
return {
|
||
...msg,
|
||
thinkingTrace: trace,
|
||
};
|
||
}));
|
||
} else {
|
||
state.current.compareResult[state.current.compareIndex][side].thinkingTrace = trace;
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
thinkingTrace: trace,
|
||
};
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理真实多轮 RAG 请求。
|
||
*
|
||
* 多轮接口采用 SSE:每行 `data:` 都描述一个事件。组件根据事件类型分别更新 taskId、工具调用、
|
||
* 最终回答和引用来源。当前用户入口暂时走本地模拟,此方法保留给接口连通后切换使用。
|
||
*/
|
||
const handleRun = async (query: string, side: 'left' | 'right' | null = null) => {
|
||
setError(null);
|
||
setIsLoading(true);
|
||
setIsPreConnectLoading(true);
|
||
try {
|
||
const response = await fetch(`${generateUrl}/generate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: query }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
setIsPreConnectLoading(false);
|
||
|
||
const assistantId = `assistant-${Date.now()}`;
|
||
const assistantMessage: Message = {
|
||
id: assistantId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
if (!side) {
|
||
state.current.messageList.push(assistantMessage); // 数据存储
|
||
// 接口连通后再展示多轮检索主题与过程
|
||
setMessages((prev) => [...prev, assistantMessage]);
|
||
} else if (side === 'left' || side === 'right') {
|
||
state.current.compareResult[state.current.compareIndex][side].thinkingTrace = createMultiRoundStreamTrace();
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
});
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
// 保存 reader 引用,用于"停止生成"功能
|
||
if (!side) {
|
||
readerRef.current = reader;
|
||
} else {
|
||
compareReaderRef.current[side] = reader;
|
||
}
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) {
|
||
console.log('聊天记录', state.current.compareResult);
|
||
if (!side) {
|
||
chatHistoryAddSearch();
|
||
}
|
||
break;
|
||
}
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
const lastLine = lines.pop();
|
||
buffer = lastLine || '';
|
||
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue;
|
||
|
||
try {
|
||
const parsedData = JSON.parse(line.slice(6)) as {
|
||
event: string;
|
||
data: any;
|
||
};
|
||
const { event, data } = parsedData;
|
||
|
||
switch (event) {
|
||
case 'task_created': {
|
||
taskIdRef.current = data?.task_id || null;
|
||
break;
|
||
}
|
||
case 'tool_call': {
|
||
handleToolCall(data as ToolCallData, assistantId, side);
|
||
break;
|
||
}
|
||
case 'message': {
|
||
if (typeof data === 'string') {
|
||
setMessages((prevMessages) => prevMessages.map((msg) => {
|
||
if (msg.id !== assistantId || !msg.thinkingTrace) return msg;
|
||
return {
|
||
...msg,
|
||
thinkingTrace: {
|
||
...msg.thinkingTrace,
|
||
finalAnswerMarkdown: `${msg.thinkingTrace.finalAnswerMarkdown}${data}`,
|
||
},
|
||
};
|
||
}));
|
||
}
|
||
break;
|
||
}
|
||
case 'references': {
|
||
const refs = Array.isArray(data?.references) ? data.references : [];
|
||
const mappedRefs = refs.map(mapReferenceToSource);
|
||
setSearchProcess((prev) => ({
|
||
...prev,
|
||
hits: mappedRefs.length,
|
||
}));
|
||
setMessages((prevMessages) => prevMessages.map((msg) => (
|
||
msg.id === assistantId
|
||
? { ...msg, sources: mappedRefs }
|
||
: msg
|
||
)));
|
||
break;
|
||
}
|
||
case 'done': {
|
||
setIsLoading(false);
|
||
break;
|
||
}
|
||
default:
|
||
console.log(`未知事件类型: ${event}`, data);
|
||
}
|
||
} catch (parseError) {
|
||
console.error('JSON解析错误:', parseError);
|
||
console.error('原始数据:', line.slice(6));
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
if (readerRef.current) {
|
||
readerRef.current.releaseLock();
|
||
readerRef.current = null;
|
||
}
|
||
}
|
||
} catch (runError) {
|
||
console.error('处理请求时发生错误:', runError);
|
||
setIsLoading(false);
|
||
setError(runError instanceof Error ? runError.message : '发生未知错误,请稍后重试');
|
||
setIsPreConnectLoading(false);
|
||
} finally {
|
||
// 无论成功还是失败,都要重置加载状态和解锁提交
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
handleStopCompareSide(side);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理任务检索 请求。
|
||
*
|
||
* 多轮接口采用 SSE:每行 `data:` 都描述一个事件。组件根据事件类型分别更新 taskId、工具调用、
|
||
* 最终回答和引用来源。当前用户入口暂时走本地模拟,此方法保留给接口连通后切换使用。
|
||
*/
|
||
const handleTaskRun = async (query: string, side: 'left' | 'right' | null = null) => {
|
||
setError(null);
|
||
setIsLoading(true);
|
||
setIsPreConnectLoading(true);
|
||
|
||
try {
|
||
const response = await fetch(`${taskChatUrl}/generate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: query }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
setIsPreConnectLoading(false);
|
||
|
||
const assistantId = `assistant-${Date.now()}`;
|
||
const assistantMessage: Message = {
|
||
id: assistantId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
if (!side) {
|
||
state.current.messageList.push(assistantMessage); // 数据存储
|
||
// 接口连通后再展示多轮检索主题与过程
|
||
setMessages((prev) => [...prev, assistantMessage]);
|
||
} else if (side === 'left' || side === 'right') {
|
||
state.current.compareResult[state.current.compareIndex][side].thinkingTrace = createMultiRoundStreamTrace();
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
});
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
if (!side) {
|
||
readerRef.current = reader;
|
||
} else {
|
||
compareReaderRef.current[side] = reader;
|
||
}
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) {
|
||
console.log('聊天记录', state.current.messageList);
|
||
chatHistoryAddSearch();
|
||
break;
|
||
}
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
const lastLine = lines.pop();
|
||
buffer = lastLine || '';
|
||
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue;
|
||
|
||
try {
|
||
const parsedData = JSON.parse(line.slice(6)) as {
|
||
event: string;
|
||
data: any;
|
||
};
|
||
const { event, data } = parsedData;
|
||
|
||
switch (event) {
|
||
case 'task_created': {
|
||
taskIdRef.current = data?.task_id || null;
|
||
break;
|
||
}
|
||
case 'tool_call': {
|
||
handleToolCall(data as ToolCallData, assistantId, side);
|
||
break;
|
||
}
|
||
case 'message': {
|
||
if (typeof data === 'string') {
|
||
setMessages((prevMessages) => prevMessages.map((msg) => {
|
||
if (msg.id !== assistantId || !msg.thinkingTrace) return msg;
|
||
return {
|
||
...msg,
|
||
thinkingTrace: {
|
||
...msg.thinkingTrace,
|
||
finalAnswerMarkdown: `${msg.thinkingTrace.finalAnswerMarkdown}${data}`,
|
||
},
|
||
};
|
||
}));
|
||
}
|
||
break;
|
||
}
|
||
case 'references': {
|
||
const refs = Array.isArray(data?.references) ? data.references : [];
|
||
const mappedRefs = refs.map(mapReferenceToSource);
|
||
setSearchProcess((prev) => ({
|
||
...prev,
|
||
hits: mappedRefs.length,
|
||
}));
|
||
setMessages((prevMessages) => prevMessages.map((msg) => (
|
||
msg.id === assistantId
|
||
? { ...msg, sources: mappedRefs }
|
||
: msg
|
||
)));
|
||
break;
|
||
}
|
||
case 'done': {
|
||
setIsLoading(false);
|
||
break;
|
||
}
|
||
default:
|
||
console.log(`未知事件类型: ${event}`, data);
|
||
}
|
||
} catch (parseError) {
|
||
console.error('JSON解析错误:', parseError);
|
||
console.error('原始数据:', line.slice(6));
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
if (readerRef.current) {
|
||
readerRef.current.releaseLock();
|
||
readerRef.current = null;
|
||
}
|
||
}
|
||
} catch (runError) {
|
||
console.error('处理请求时发生错误:', runError);
|
||
setIsLoading(false);
|
||
setError(runError instanceof Error ? runError.message : '发生未知错误,请稍后重试');
|
||
setIsPreConnectLoading(false);
|
||
} finally {
|
||
// 无论成功还是失败,都要重置加载状态和解锁提交
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
handleStopCompareSide(side);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理画像检索 请求。
|
||
*
|
||
* 多轮接口采用 SSE:每行 `data:` 都描述一个事件。组件根据事件类型分别更新 taskId、工具调用、
|
||
* 最终回答和引用来源。当前用户入口暂时走本地模拟,此方法保留给接口连通后切换使用。
|
||
*/
|
||
const handleHxRun = async (query: string, side: 'left' | 'right' | null = null) => {
|
||
setError(null);
|
||
setIsLoading(true);
|
||
setIsPreConnectLoading(true);
|
||
|
||
try {
|
||
const response = await fetch(`${taskChatUrl}/generate_b2`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: query }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
setIsPreConnectLoading(false);
|
||
|
||
const assistantId = `assistant-${Date.now()}`;
|
||
const assistantMessage: Message = {
|
||
id: assistantId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
if (!side) {
|
||
state.current.messageList.push(assistantMessage); // 数据存储
|
||
// 接口连通后再展示多轮检索主题与过程
|
||
setMessages((prev) => [...prev, assistantMessage]);
|
||
} else if (side === 'left' || side === 'right') {
|
||
state.current.compareResult[state.current.compareIndex][side].thinkingTrace = createMultiRoundStreamTrace();
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
});
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
if (!side) {
|
||
readerRef.current = reader;
|
||
} else {
|
||
compareReaderRef.current[side] = reader;
|
||
}
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) {
|
||
console.log('聊天记录', state.current.messageList);
|
||
chatHistoryAddSearch();
|
||
break;
|
||
}
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
const lastLine = lines.pop();
|
||
buffer = lastLine || '';
|
||
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue;
|
||
|
||
try {
|
||
const parsedData = JSON.parse(line.slice(6)) as {
|
||
event: string;
|
||
data: any;
|
||
};
|
||
const { event, data } = parsedData;
|
||
|
||
switch (event) {
|
||
case 'task_created': {
|
||
taskIdRef.current = data?.task_id || null;
|
||
break;
|
||
}
|
||
case 'tool_call': {
|
||
handleToolCall(data as ToolCallData, assistantId);
|
||
break;
|
||
}
|
||
case 'message': {
|
||
if (typeof data === 'string') {
|
||
setMessages((prevMessages) => prevMessages.map((msg) => {
|
||
if (msg.id !== assistantId || !msg.thinkingTrace) return msg;
|
||
return {
|
||
...msg,
|
||
thinkingTrace: {
|
||
...msg.thinkingTrace,
|
||
finalAnswerMarkdown: `${msg.thinkingTrace.finalAnswerMarkdown}${data}`,
|
||
},
|
||
};
|
||
}));
|
||
}
|
||
break;
|
||
}
|
||
case 'references': {
|
||
const refs = Array.isArray(data?.references) ? data.references : [];
|
||
const mappedRefs = refs.map(mapReferenceToSource);
|
||
setSearchProcess((prev) => ({
|
||
...prev,
|
||
hits: mappedRefs.length,
|
||
}));
|
||
setMessages((prevMessages) => prevMessages.map((msg) => (
|
||
msg.id === assistantId
|
||
? { ...msg, sources: mappedRefs }
|
||
: msg
|
||
)));
|
||
break;
|
||
}
|
||
case 'done': {
|
||
setIsLoading(false);
|
||
break;
|
||
}
|
||
default:
|
||
console.log(`未知事件类型: ${event}`, data);
|
||
}
|
||
} catch (parseError) {
|
||
console.error('JSON解析错误:', parseError);
|
||
console.error('原始数据:', line.slice(6));
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
if (readerRef.current) {
|
||
readerRef.current.releaseLock();
|
||
readerRef.current = null;
|
||
}
|
||
}
|
||
} catch (runError) {
|
||
console.error('处理请求时发生错误:', runError);
|
||
setIsLoading(false);
|
||
setError(runError instanceof Error ? runError.message : '发生未知错误,请稍后重试');
|
||
setIsPreConnectLoading(false);
|
||
} finally {
|
||
// 无论成功还是失败,都要重置加载状态和解锁提交
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
handleStopCompareSide(side);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理画像检索 请求。
|
||
*
|
||
* 多轮接口采用 SSE:每行 `data:` 都描述一个事件。组件根据事件类型分别更新 taskId、工具调用、
|
||
* 最终回答和引用来源。当前用户入口暂时走本地模拟,此方法保留给接口连通后切换使用。
|
||
*/
|
||
const newsReportRun = async (query: string, side: 'left' | 'right' | null = null) => {
|
||
setError(null);
|
||
setIsLoading(true);
|
||
setIsPreConnectLoading(true);
|
||
|
||
try {
|
||
const response = await fetch(`${taskChatUrl}/generate_report`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: query }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
setIsPreConnectLoading(false);
|
||
|
||
const assistantId = `assistant-${Date.now()}`;
|
||
const assistantMessage: Message = {
|
||
id: assistantId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
if (!side) {
|
||
state.current.messageList.push(assistantMessage); // 数据存储
|
||
// 接口连通后再展示多轮检索主题与过程
|
||
setMessages((prev) => [...prev, assistantMessage]);
|
||
} else if (side === 'left' || side === 'right') {
|
||
state.current.compareResult[state.current.compareIndex][side].thinkingTrace = createMultiRoundStreamTrace();
|
||
updatePane(side, (pane) => {
|
||
return {
|
||
...pane,
|
||
thinkingTrace: createMultiRoundStreamTrace(),
|
||
};
|
||
});
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
if (!side) {
|
||
readerRef.current = reader;
|
||
} else {
|
||
compareReaderRef.current[side] = reader;
|
||
}
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) {
|
||
console.log('聊天记录', state.current.messageList);
|
||
chatHistoryAddSearch();
|
||
break;
|
||
}
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split('\n');
|
||
const lastLine = lines.pop();
|
||
buffer = lastLine || '';
|
||
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue;
|
||
|
||
try {
|
||
const parsedData = JSON.parse(line.slice(6)) as {
|
||
event: string;
|
||
data: any;
|
||
};
|
||
const { event, data } = parsedData;
|
||
|
||
switch (event) {
|
||
case 'task_created': {
|
||
taskIdRef.current = data?.task_id || null;
|
||
break;
|
||
}
|
||
case 'tool_call': {
|
||
handleToolCall(data as ToolCallData, assistantId);
|
||
break;
|
||
}
|
||
case 'message': {
|
||
if (typeof data === 'string') {
|
||
setMessages((prevMessages) => prevMessages.map((msg) => {
|
||
if (msg.id !== assistantId || !msg.thinkingTrace) return msg;
|
||
return {
|
||
...msg,
|
||
thinkingTrace: {
|
||
...msg.thinkingTrace,
|
||
finalAnswerMarkdown: `${msg.thinkingTrace.finalAnswerMarkdown}${data}`,
|
||
},
|
||
};
|
||
}));
|
||
}
|
||
break;
|
||
}
|
||
case 'references': {
|
||
const refs = Array.isArray(data?.references) ? data.references : [];
|
||
const mappedRefs = refs.map(mapReferenceToSource);
|
||
setSearchProcess((prev) => ({
|
||
...prev,
|
||
hits: mappedRefs.length,
|
||
}));
|
||
setMessages((prevMessages) => prevMessages.map((msg) => (
|
||
msg.id === assistantId
|
||
? { ...msg, sources: mappedRefs }
|
||
: msg
|
||
)));
|
||
break;
|
||
}
|
||
case 'done': {
|
||
setIsLoading(false);
|
||
break;
|
||
}
|
||
default:
|
||
console.log(`未知事件类型: ${event}`, data);
|
||
}
|
||
} catch (parseError) {
|
||
console.error('JSON解析错误:', parseError);
|
||
console.error('原始数据:', line.slice(6));
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
if (readerRef.current) {
|
||
readerRef.current.releaseLock();
|
||
readerRef.current = null;
|
||
}
|
||
}
|
||
} catch (runError) {
|
||
console.error('处理请求时发生错误:', runError);
|
||
setIsLoading(false);
|
||
setError(runError instanceof Error ? runError.message : '发生未知错误,请稍后重试');
|
||
setIsPreConnectLoading(false);
|
||
} finally {
|
||
// 无论成功还是失败,都要重置加载状态和解锁提交
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
handleStopCompareSide(side);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 单一模式下的发送入口。
|
||
*
|
||
* 这里只负责创建用户消息、重置输入框和按当前 qaMode 分发到对应执行器;
|
||
* 具体的真实请求、模拟流和状态回填分别在各自 handler 内维护。
|
||
*/
|
||
const handleSend = () => {
|
||
if (submitLockRef.current || isLoading || isPreConnectLoading) return;
|
||
if (!inputValue.trim()) return;
|
||
submitLockRef.current = true;
|
||
const query = inputValue.trim();
|
||
state.current.messageList = [];
|
||
const userMessage: Message = {
|
||
id: Date.now().toString(),
|
||
role: 'user',
|
||
content: query,
|
||
timestamp: new Date(),
|
||
};
|
||
state.current.messageList.push(userMessage);
|
||
if (qaMode === 'multi-round') { // 多轮检索
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
setSearchProcess({
|
||
currentMode: '多轮RAG',
|
||
rounds: 0,
|
||
hits: 0,
|
||
queryRewrite: '',
|
||
});
|
||
void handleRun(query);
|
||
return;
|
||
}
|
||
|
||
if (qaMode === 'deep-search') { // 任务检索
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
setSearchProcess({
|
||
currentMode: '任务检索',
|
||
rounds: 0,
|
||
hits: 0,
|
||
queryRewrite: '',
|
||
});
|
||
void handleTaskRun(query);
|
||
return;
|
||
}
|
||
|
||
if (qaMode === 'hx-search') { // 任务检索
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
setSearchProcess({
|
||
currentMode: '画像检索',
|
||
rounds: 0,
|
||
hits: 0,
|
||
queryRewrite: '',
|
||
});
|
||
void handleHxRun(query);
|
||
return;
|
||
}
|
||
|
||
if (qaMode === 'news-report') { // 新闻报告生成
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
setSearchProcess({
|
||
currentMode: '新闻报告生成',
|
||
rounds: 0,
|
||
hits: 0,
|
||
queryRewrite: '',
|
||
});
|
||
void newsReportRun(query);
|
||
return;
|
||
}
|
||
|
||
if (qaMode === 'single-round') { // 单轮RAG
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
setSearchProcess({
|
||
currentMode: '单轮RAG',
|
||
rounds: 1,
|
||
hits: 0,
|
||
queryRewrite: '',
|
||
});
|
||
void handleSingleRoundRun(query);
|
||
return;
|
||
}
|
||
|
||
if (qaMode === 'single-chat') { // 大模型问答
|
||
console.log('进入了这里')
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
setSearchProcess({
|
||
currentMode: '大模型问答',
|
||
rounds: 1,
|
||
hits: 0,
|
||
queryRewrite: '',
|
||
});
|
||
void chatRun(query);
|
||
// handleMockSingleRoundRun(query);
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
setMessages((prev) => [...prev, userMessage]);
|
||
setInputValue('');
|
||
};
|
||
|
||
/**
|
||
* 对比模式发送入口。
|
||
*
|
||
* 左右面板共享同一个用户问题,但各自按选择的模式独立运行、独立 loading、独立 sources。
|
||
* 通过 `updatePane` 将左右面板更新收敛到一个函数,避免并发请求互相覆盖状态。
|
||
*/
|
||
const handleCompare = () => {
|
||
if (submitLockRef.current || isLoading || isPreConnectLoading) return;
|
||
if (!inputValue.trim()) return;
|
||
submitLockRef.current = true;
|
||
const query = inputValue.trim();
|
||
setInputValue('');
|
||
setCompareUserInput(query);
|
||
setError(null);
|
||
setIsLoading(true);
|
||
const compareId = Date.now().toString();
|
||
console.log('compareLeftMode',compareLeftMode)
|
||
const compareItem = {
|
||
id: compareId,
|
||
compareUserInput: query,
|
||
left: createComparePane(compareLeftMode),
|
||
right: createComparePane(compareRightMode),
|
||
}
|
||
setCompareResult((pev) => [...pev, compareItem]);
|
||
state.current.compareResult = [];
|
||
state.current.compareId = compareId;
|
||
state.current.compareResult.push(compareItem);
|
||
state.current.compareIndex = state.current.compareResult.length - 1;
|
||
console.log('compareItem', compareItem)
|
||
const runByMode = (side: 'left' | 'right', mode: QAMode) => {
|
||
if (mode === 'single-chat') { // 大模型问答
|
||
return chatRun(query, side);
|
||
} else if (mode === 'single-round') { // 单轮rag检索
|
||
return handleSingleRoundRun(query, side);
|
||
} else if (mode === 'multi-round') { // 多轮检索
|
||
return handleRun(query, side);
|
||
} else if (mode === 'deep-search') { // 任务检索
|
||
return handleTaskRun(query, side);
|
||
} else if (mode === 'hx-search') { // 画像检索
|
||
return handleHxRun(query, side);
|
||
} else if (mode === 'news-report') { // 新闻检索
|
||
return handleHxRun(query, side);
|
||
}
|
||
};
|
||
|
||
Promise.allSettled([
|
||
runByMode('left', compareLeftMode).finally(() => updatePane('left', (pane) => ({ ...pane, loading: false }))),
|
||
runByMode('right', compareRightMode).finally(() => updatePane('right', (pane) => ({ ...pane, loading: false }))),
|
||
]).finally(() => {
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
chatHistoryAddSearch();
|
||
});
|
||
};
|
||
|
||
const handleStopCompareSide = (side: 'left' | 'right') => {
|
||
const reader = compareReaderRef.current[side];
|
||
if (reader) {
|
||
void reader.cancel(`用户停止${side === 'left' ? '左侧' : '右侧'}生成`);
|
||
compareReaderRef.current[side] = null;
|
||
}
|
||
updatePane(side, (pane) => ({ ...pane, loading: false }))
|
||
};
|
||
|
||
/**
|
||
* 停止当前生成。
|
||
*
|
||
* 真实请求通过 reader.cancel() 中断读取;本地模拟通过 ref 标记让异步循环自行退出。
|
||
* 对比模式需要同时停止左右两侧,单一模式只处理当前主 reader。
|
||
*/
|
||
const handleStop = () => {
|
||
if (viewMode === 'compare') {
|
||
handleStopCompareSide('left');
|
||
handleStopCompareSide('right');
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
return;
|
||
}
|
||
mockSingleRoundStoppedRef.current = true;
|
||
mockMultiRoundStoppedRef.current = true;
|
||
if (readerRef.current) {
|
||
void readerRef.current.cancel('用户手动停止');
|
||
readerRef.current = null;
|
||
}
|
||
setIsPreConnectLoading(false);
|
||
setIsLoading(false);
|
||
submitLockRef.current = false;
|
||
};
|
||
|
||
/**
|
||
* 对“生成中切换页面/模式/布局”进行统一保护。
|
||
*
|
||
* 如果当前没有生成任务,直接执行目标操作;否则弹出确认框。
|
||
* 用户确认后先停止生成,再执行原操作,避免生成流继续写入已经切换的 UI 状态。
|
||
*/
|
||
const runAfterGenerationConfirm = (action: () => void) => {
|
||
if (!isLoading) {
|
||
action();
|
||
return;
|
||
}
|
||
pendingActionRef.current = action;
|
||
// Dialog.confirm({
|
||
// header: '模型正在生成',
|
||
// body: '当前模型仍在生成内容。确认后将停止生成,并继续执行你的操作。',
|
||
// confirmBtn: '确认停止并继续',
|
||
// cancelBtn: '取消',
|
||
// onConfirm: () => {
|
||
// handleStop();
|
||
// pendingActionRef.current?.();
|
||
// pendingActionRef.current = null;
|
||
// },
|
||
// onClose: () => {
|
||
// pendingActionRef.current = null;
|
||
// },
|
||
// });
|
||
};
|
||
|
||
const handleSafeViewModeChange = (mode: ViewMode) => {
|
||
if (mode === viewMode) return;
|
||
runAfterGenerationConfirm(() => setViewMode(mode));
|
||
};
|
||
|
||
const handleSafeNavigate = (page: PageType) => {
|
||
runAfterGenerationConfirm(() => onNavigate(page));
|
||
};
|
||
|
||
const handleSafeReset = () => {
|
||
runAfterGenerationConfirm(() => {
|
||
setMessages([]);
|
||
setCompareResult(null);
|
||
});
|
||
};
|
||
|
||
const handleSafeModeChange = (mode: QAMode) => {
|
||
runAfterGenerationConfirm(() => setQaMode(mode));
|
||
};
|
||
|
||
const handleSafeCompareLeftModeChange = (mode: QAMode) => {
|
||
runAfterGenerationConfirm(() => setCompareLeftMode(mode));
|
||
};
|
||
|
||
const handleSafeCompareRightModeChange = (mode: QAMode) => {
|
||
runAfterGenerationConfirm(() => setCompareRightMode(mode));
|
||
};
|
||
|
||
const handleSafeCompareLayoutChange = (layout: 'horizontal' | 'vertical') => {
|
||
runAfterGenerationConfirm(() => setCompareLayout(layout));
|
||
};
|
||
|
||
const handleSaveRecord = () => {
|
||
alert('问答记录已保存');
|
||
};
|
||
|
||
const handleCollect = () => {
|
||
alert('已添加到收藏');
|
||
};
|
||
|
||
const handleKeyDown = (e) => {
|
||
// if (e.key === 'Enter' && !e.shiftKey) {
|
||
// console.log('回车了')
|
||
// e.preventDefault();
|
||
// if (e.repeat || submitLockRef.current || isLoading || isPreConnectLoading) return;
|
||
// if (viewMode === 'compare') {
|
||
// handleCompare();
|
||
// } else {
|
||
// handleSend();
|
||
// }
|
||
// }
|
||
// console.log('e',e)
|
||
// if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||
// e.preventDefault();
|
||
// } else if (e.keyCode === 13) {
|
||
// e.preventDefault();
|
||
// console.log('回车了')
|
||
// } else if (e.key === "/") {
|
||
// e.preventDefault();
|
||
// }
|
||
// if (e.repeat || submitLockRef.current || isLoading || isPreConnectLoading) return;
|
||
state.current.messageList = [];
|
||
if (viewMode === 'compare') {
|
||
handleCompare();
|
||
} else {
|
||
handleSend();
|
||
}
|
||
setIsAutoScrollEnabled(true);
|
||
setShowScrollToBottom(false);
|
||
scrollToBottom(true);
|
||
};
|
||
|
||
// 获取模式标签
|
||
const getModeLabel = (mode: QAMode) => {
|
||
switch (mode) {
|
||
case 'single-chat': return '大模型';
|
||
case 'single-round': return '单轮RAG';
|
||
case 'multi-round': return '多轮RAG';
|
||
case 'deep-search': return '任务检索';
|
||
case 'hx-search': return '画像检索';
|
||
case 'news-report': return '新闻报告生成';
|
||
}
|
||
};
|
||
|
||
const activeAssistantMessageId = isLoading
|
||
? [...messages].reverse().find((msg) => msg.role === 'assistant')?.id
|
||
: undefined;
|
||
|
||
useEffect(() => {
|
||
setQaMode('hx-search');
|
||
// setMessages(createMockSingleRagMessages());
|
||
// setSearchProcess({
|
||
// currentMode: '单轮RAG',
|
||
// rounds: 1,
|
||
// hits: 3,
|
||
// queryRewrite: '',
|
||
// });
|
||
chatDataSearch();
|
||
}, [viewMode]);
|
||
|
||
// 创建聊天窗口
|
||
const addChatDataChange = async () => {
|
||
let qaMode = 'SINGLE_RAG';
|
||
if (viewMode === 'compare') {
|
||
qaMode = 'MULTI_RAG'
|
||
}
|
||
const parmy = {
|
||
userId: userId.current,
|
||
qaType: qaType.current,
|
||
questionTitle: '新建问答',
|
||
qaMode: qaMode,
|
||
}
|
||
const res = await service.post(`${zncmUrl}/llm/qa/history/create`, parmy);
|
||
console.log(res);
|
||
const result = res.data;
|
||
chatDataId.current = result.id;
|
||
if (viewMode === 'compare') {
|
||
compareChatId.current = result.id;
|
||
} else {
|
||
chatDataId.current = result.id;
|
||
}
|
||
}
|
||
|
||
// 查找聊天记录窗口
|
||
const chatDataSearch = async () => {
|
||
let qaMode = 'SINGLE_RAG';
|
||
console.log('viewMode', viewMode)
|
||
if (viewMode === 'compare') {
|
||
qaMode = 'MULTI_RAG'
|
||
}
|
||
|
||
const parmy = {
|
||
userId: userId.current,
|
||
qaType: qaType.current,
|
||
qaMode: qaMode,
|
||
current: 1,
|
||
size: 10
|
||
}
|
||
const res = await service.post(`${zncmUrl}/llm/qa/history/page`, parmy);
|
||
console.log('res', res);
|
||
const result = res.data;
|
||
if (result.length === 0) {
|
||
await addChatDataChange();
|
||
} else if (result.length > 0) {
|
||
if (viewMode === 'compare') {
|
||
compareChatId.current = result[0].id;
|
||
} else {
|
||
chatDataId.current = result[0].id;
|
||
}
|
||
}
|
||
await chatHistorySearch();
|
||
console.log('chatDataId', chatDataId.current, compareChatId.current)
|
||
}
|
||
|
||
// 根据对话记录窗口查找对话记录
|
||
const chatHistorySearch = async () => {
|
||
let chatHistoryId = chatDataId.current;
|
||
if (viewMode === 'compare') {
|
||
chatHistoryId = compareChatId.current;
|
||
}
|
||
const res = await service.get(`${zncmUrl}/llm/qa/history/pairs/${chatHistoryId}`, '');
|
||
console.log('问答记录', res);
|
||
// 1. 修正变量名 result
|
||
const result = res.data;
|
||
if (viewMode !== 'compare') {
|
||
// 2. 使用 flatMap 将二维数组拍平,并生成新的消息列表
|
||
const messageList: Message[] = result.flatMap((item, itemIndex) => {
|
||
// 3. 安全解析 JSON,使用 ?? 提供空数组兜底
|
||
const parsedAnswers = JSON.parse(item.answer ?? '[]');
|
||
|
||
// 4. map 处理每一条 answer,生成新的对象(不修改原对象)
|
||
return parsedAnswers.map((aItem: any, aIndex: number) => {
|
||
return {
|
||
...aItem,
|
||
// 优化 ID 赋值逻辑:建议不管是不是索引 1,都补全 ID,防止遗漏
|
||
// 如果必须只针对第二条,保留原逻辑,但增加安全性
|
||
id: aIndex === 1 ? String(item.id) : (aItem.id || `temp-${itemIndex}-${aIndex}`),
|
||
|
||
// 如果后端返回的数据缺少 timestamp 或 role,最好在这里补全默认值
|
||
timestamp: aItem.timestamp ? new Date(aItem.timestamp) : new Date(),
|
||
role: aItem.role || 'assistant',
|
||
} as Message;
|
||
});
|
||
});
|
||
console.log('result', messageList); // 此时 result 保持纯净,未被修改
|
||
setMessages(messageList);
|
||
} else {
|
||
console.log(result);
|
||
const messageList = result.flatMap(item => {
|
||
const parsedAnswers = JSON.parse(item.answer ?? '[]');
|
||
return parsedAnswers.map(aItem => ({
|
||
...aItem,
|
||
left: {
|
||
...aItem.left,
|
||
loading: false,
|
||
},
|
||
right: {
|
||
...aItem.right,
|
||
loading: false,
|
||
},
|
||
id: item.id,
|
||
}));
|
||
});
|
||
console.log('messageList', messageList)
|
||
setCompareResult(messageList);
|
||
}
|
||
// state.current.messageList = reuslt || [];
|
||
}
|
||
|
||
// 创建记录窗口
|
||
const chatHistoryAddSearch = async () => {
|
||
let historyId = chatDataId.current;
|
||
let answer = '';
|
||
let question = '';
|
||
if (viewMode === 'compare') {
|
||
historyId = compareChatId.current;
|
||
answer = JSON.stringify(state.current.compareResult);
|
||
question = state.current.compareResult[0].compareUserInput;
|
||
} else {
|
||
answer = JSON.stringify(state.current.messageList);
|
||
question = state.current.messageList[0].content;
|
||
}
|
||
const param = {
|
||
historyId: historyId,
|
||
userId: userId.current,
|
||
answer: answer,
|
||
question: question,
|
||
}
|
||
const res = await service.post(`${zncmUrl}/llm/qa/history/pair/add`, param);
|
||
console.log('添加聊天记录窗口', res);
|
||
}
|
||
|
||
/**
|
||
* 滚动到底部。
|
||
*
|
||
* 聊天区默认跟随新消息自动滚动;当用户手动上滑查看历史时会暂停自动滚动,避免打断阅读。
|
||
*/
|
||
const scrollToBottom = (smooth = false) => {
|
||
if (!chatListRef.current) return;
|
||
chatListRef.current.scrollTo({
|
||
top: chatListRef.current.scrollHeight,
|
||
behavior: smooth ? 'smooth' : 'auto',
|
||
});
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (isAutoScrollEnabled) {
|
||
scrollToBottom(false);
|
||
}
|
||
}, [messages, isLoading, isAutoScrollEnabled, compareResult]);
|
||
|
||
const handleChatScroll = () => {
|
||
if (!chatListRef.current) return;
|
||
const { scrollTop, scrollHeight, clientHeight } = chatListRef.current;
|
||
const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 80;
|
||
setIsAutoScrollEnabled(isNearBottom);
|
||
setShowScrollToBottom(!isNearBottom);
|
||
};
|
||
|
||
const getAssistantAnswerText = (message: Message) => (
|
||
message.singleRagTrace?.answerMarkdown || message.thinkingTrace?.finalAnswerMarkdown || message.content
|
||
);
|
||
|
||
const getAssistantThinkingText = (message: Message) => {
|
||
if (message.singleRagTrace) return message.singleRagTrace.thinkingMarkdown;
|
||
if (message.thinkingTrace) {
|
||
return message.thinkingTrace.rounds.map((round, index) => (
|
||
`## 第 ${index + 1} 轮:${round.title}\n${round.thoughtMarkdown}`
|
||
)).join('\n\n');
|
||
}
|
||
return '';
|
||
};
|
||
|
||
const handleCopyAssistantMessage = async (message: Message) => {
|
||
const answerText = getAssistantAnswerText(message);
|
||
try {
|
||
await navigator.clipboard.writeText(answerText);
|
||
} catch (copyError) {
|
||
console.error('复制模型回答失败:', copyError);
|
||
}
|
||
};
|
||
|
||
const handleDeleteAssistantMessage = (message: Message) => {
|
||
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
|
||
const previousUserMessage = [...messages.slice(0, messageIndex)].reverse().find((msg) => msg.role === 'user');
|
||
setMessages((prev) => prev.filter((msg) => msg.id !== message.id && msg.id !== previousUserMessage?.id));
|
||
};
|
||
|
||
const handleEditAssistantMessage = (message: Message) => {
|
||
setEditingMessageId(message.id);
|
||
setEditingThinking(getAssistantThinkingText(message));
|
||
setEditingAnswer(getAssistantAnswerText(message));
|
||
};
|
||
|
||
const handleCancelAssistantEdit = () => {
|
||
setEditingMessageId(null);
|
||
setEditingThinking('');
|
||
setEditingAnswer('');
|
||
};
|
||
|
||
const handleSaveAssistantEdit = () => {
|
||
if (!editingMessageId) return;
|
||
setMessages((prev) => prev.map((msg) => {
|
||
if (msg.id !== editingMessageId) return msg;
|
||
if (msg.singleRagTrace) {
|
||
return {
|
||
...msg,
|
||
content: editingAnswer,
|
||
singleRagTrace: {
|
||
...msg.singleRagTrace,
|
||
thinkingMarkdown: editingThinking,
|
||
answerMarkdown: editingAnswer,
|
||
thinkingComplete: true,
|
||
},
|
||
};
|
||
}
|
||
if (msg.thinkingTrace) {
|
||
const rounds = msg.thinkingTrace.rounds.length > 0
|
||
? msg.thinkingTrace.rounds.map((round, index) => (
|
||
index === 0 ? { ...round, thoughtMarkdown: editingThinking } : round
|
||
))
|
||
: msg.thinkingTrace.rounds;
|
||
return {
|
||
...msg,
|
||
content: editingAnswer,
|
||
thinkingTrace: {
|
||
...msg.thinkingTrace,
|
||
rounds,
|
||
finalAnswerMarkdown: editingAnswer,
|
||
},
|
||
};
|
||
}
|
||
return { ...msg, content: editingAnswer };
|
||
}));
|
||
handleCancelAssistantEdit();
|
||
};
|
||
|
||
const handleResendAssistantMessage = (message: Message) => {
|
||
if (submitLockRef.current || isLoading || isPreConnectLoading) return;
|
||
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
|
||
const previousUserMessage = [...messages.slice(0, messageIndex)].reverse().find((msg) => msg.role === 'user');
|
||
if (!previousUserMessage) return;
|
||
|
||
submitLockRef.current = true;
|
||
setMessages((prev) => prev.filter((msg) => msg.id !== message.id));
|
||
setInputValue('');
|
||
|
||
if (message.singleRagTrace) {
|
||
setSearchProcess({ currentMode: '单轮RAG', rounds: 1, hits: 0, queryRewrite: '' });
|
||
void handleSingleRoundRun(previousUserMessage.content);
|
||
return;
|
||
}
|
||
|
||
setSearchProcess({ currentMode: '多轮RAG', rounds: 0, hits: 0, queryRewrite: '' });
|
||
void handleRun(previousUserMessage.content);
|
||
};
|
||
|
||
/**
|
||
* 点击答案中的引用编号后打开引用详情弹窗。
|
||
*
|
||
* Markdown 中的 `[[1]]` 与 sources 数组采用 1-based 对齐,因此这里需要减 1 取数组下标。
|
||
*/
|
||
const handleCitationClick = (message: Message, citationIndex: number) => {
|
||
const source = message.sources?.[citationIndex - 1];
|
||
if (!source) return;
|
||
setSelectedCitation(source);
|
||
};
|
||
|
||
const handleCompareCitationClick = (side: 'left' | 'right', citationIndex: number) => {
|
||
const source = compareResult?.[side]?.sources?.[citationIndex - 1];
|
||
if (!source) return;
|
||
setSelectedCitation(source);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (viewMode === 'compare') {
|
||
setCompareLeftMode('single-round');
|
||
setCompareRightMode('multi-round');
|
||
}
|
||
}, [viewMode]);
|
||
|
||
useEffect(() => {
|
||
if (viewMode !== 'compare' || !compareResult) return;
|
||
// setIsLoading(compareResult.left.loading || compareResult.right.loading);
|
||
}, [viewMode, compareResult]);
|
||
|
||
// 离开浏览器页面时无法展示自定义 Dialog,因此使用原生 beforeunload 提醒用户生成会被中断。
|
||
// useEffect(() => {
|
||
// const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||
// if (!isLoading) return;
|
||
// event.preventDefault();
|
||
// event.returnValue = '模型正在生成,离开页面将停止当前生成。';
|
||
// };
|
||
|
||
// window.addEventListener('beforeunload', handleBeforeUnload);
|
||
// return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||
// }, [isLoading]);
|
||
|
||
return (
|
||
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||
<GeneralQAHeader
|
||
chatHeadTitle={'课题研究'}
|
||
chatDesign={'支持任务检索,画像检索,新闻报告生成'}
|
||
compareShow={false}
|
||
isDark={isDark}
|
||
viewMode={viewMode}
|
||
setViewMode={handleSafeViewModeChange}
|
||
onNavigate={handleSafeNavigate}
|
||
/>
|
||
|
||
{/* 主体内容区 */}
|
||
<div className="flex-1 flex overflow-hidden">
|
||
<GeneralQAConfigSidebar
|
||
isDark={isDark}
|
||
viewMode={viewMode}
|
||
qaMode={qaMode}
|
||
compareLeftMode={compareLeftMode}
|
||
compareRightMode={compareRightMode}
|
||
compareLayout={compareLayout}
|
||
knowledgeScope={knowledgeScope}
|
||
answerStyle={answerStyle}
|
||
showSources={showSources}
|
||
showProcess={showProcess}
|
||
autoSummarizeDiff={autoSummarizeDiff}
|
||
recommendBetter={recommendBetter}
|
||
isCollapsed={isLeftSidebarCollapsed}
|
||
qaModeOptions={qaModeList}
|
||
setQaMode={handleSafeModeChange}
|
||
setCompareLeftMode={handleSafeCompareLeftModeChange}
|
||
setCompareRightMode={handleSafeCompareRightModeChange}
|
||
setCompareLayout={handleSafeCompareLayoutChange}
|
||
setKnowledgeScope={setKnowledgeScope}
|
||
setAnswerStyle={setAnswerStyle}
|
||
setShowSources={setShowSources}
|
||
setShowProcess={setShowProcess}
|
||
setAutoSummarizeDiff={setAutoSummarizeDiff}
|
||
setRecommendBetter={setRecommendBetter}
|
||
setIsCollapsed={setIsLeftSidebarCollapsed}
|
||
/>
|
||
|
||
{/* 中间结果区 */}
|
||
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
||
{viewMode === 'single' ? (
|
||
<>
|
||
{/* 聊天消息区 */}
|
||
<div
|
||
ref={chatListRef}
|
||
onScroll={handleChatScroll}
|
||
className="relative flex-1 overflow-y-auto p-4 space-y-4"
|
||
>
|
||
{messages.length === 0 ? (
|
||
<EmptyChatState
|
||
icon={<MessageSquare className="w-11 h-11 opacity-60" />}
|
||
title="开始你的问答"
|
||
description="输入问题,获取基于知识库的智能回答"
|
||
/>
|
||
) : (
|
||
messages.map((msg) => (
|
||
<ChatMessage
|
||
key={msg.id}
|
||
message={msg}
|
||
showSources={showSources}
|
||
assistantLabel="知识库助手"
|
||
assistantActive={msg.id === activeAssistantMessageId}
|
||
onCitationClick={handleCitationClick}
|
||
onCopyAssistant={handleCopyAssistantMessage}
|
||
onDeleteAssistant={handleDeleteAssistantMessage}
|
||
onEditAssistant={handleEditAssistantMessage}
|
||
onResendAssistant={handleResendAssistantMessage}
|
||
/>
|
||
))
|
||
)}
|
||
|
||
{isPreConnectLoading && (
|
||
<TypingIndicator
|
||
label={qaMode === 'single-round' ? '正在流式生成单轮 RAG 回复' : '正在生成思考过程与检索内容'}
|
||
assistantLabel="知识库助手"
|
||
/>
|
||
)}
|
||
|
||
{error && (
|
||
<div className={`ml-12 max-w-[70%] rounded-xl border px-4 py-3 text-sm ${isDark
|
||
? 'bg-red-900/20 border-red-700/50 text-red-300'
|
||
: 'bg-red-50 border-red-200 text-red-700'
|
||
}`}>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{showScrollToBottom && (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setIsAutoScrollEnabled(true);
|
||
setShowScrollToBottom(false);
|
||
scrollToBottom(true);
|
||
}}
|
||
className={`fixed left-1/2 -translate-x-1/2 bottom-36 z-20 w-11 h-11 rounded-full backdrop-blur-sm border shadow-lg transition-all ${isDark
|
||
? 'bg-slate-800/65 border-slate-500/60 text-slate-100 hover:bg-slate-700/70'
|
||
: 'bg-white/70 border-gray-300 text-gray-700 hover:bg-white/85'
|
||
}`}
|
||
title="滚动到底部"
|
||
>
|
||
<ArrowDown className="w-5 h-5 mx-auto" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div ref={chatListRef} onScroll={handleChatScroll} className="flex-1 overflow-y-auto p-4">
|
||
{/* 对比结果区 */}
|
||
{
|
||
compareResult.length === 0 && <div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||
<GitCompare className="w-16 h-16 mb-4 opacity-50" />
|
||
<p className="text-lg">对比模式</p>
|
||
<p className="text-sm mt-2">输入问题,对比不同模式的回答效果</p>
|
||
</div>
|
||
}
|
||
{
|
||
compareResult.map((compare: CompareResult, index: number) => (
|
||
<div className="mb-[10px]" key={index}>
|
||
<div className="mb-4 flex justify-end">
|
||
<div className={`max-w-[72%] rounded-2xl px-4 py-3 shadow-sm ${isDark
|
||
? 'bg-blue-600/70 text-white'
|
||
: 'bg-gradient-to-r from-red-500 to-rose-500 text-white'
|
||
}`}>
|
||
<p className="whitespace-pre-wrap leading-relaxed">{compare.compareUserInput}</p>
|
||
</div>
|
||
</div>
|
||
{compareResult ? (
|
||
compareLayout === 'horizontal' ? (
|
||
<div className="grid grid-cols-2 gap-4 min-w-0">
|
||
{/* 左侧结果 */}
|
||
<div className={`min-w-0 rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<QAAvatar role="assistant" variant="blue" isActive={compare.left.loading} label="知识库助手" />
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||
}`}>
|
||
{getModeLabel(compare.left.mode)}
|
||
</span>
|
||
</div>
|
||
{compare.left.loading && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleStopCompareSide('left')}
|
||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs ${isDark ? 'bg-rose-600/80 hover:bg-rose-600 text-white' : 'bg-red-500 hover:bg-red-600 text-white'
|
||
}`}
|
||
>
|
||
<Square className="w-3 h-3 fill-current" />
|
||
停止
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 min-w-0">
|
||
<div className="flex-1 min-w-0">
|
||
{(compare.left.mode === 'single-round' || compare.left.mode === 'single-chat')? (
|
||
<SingleRagAnswer trace={compare.left.singleRagTrace || parseSingleRagStream([])} onCitationClick={(idx) => handleCompareCitationClick('left', idx)} />
|
||
) : (
|
||
<RetrievalThinkingTrace trace={compare.left.thinkingTrace || createMultiRoundStreamTrace()} onCitationClick={(idx) => handleCompareCitationClick('left', idx)} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 右侧结果 */}
|
||
<div className={`min-w-0 overflow-hidden rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<QAAvatar role="assistant" variant="purple" isActive={compare.right.loading} label="知识库助手" />
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||
}`}>
|
||
{getModeLabel(compare.right.mode)}
|
||
</span>
|
||
</div>
|
||
{compare.right.loading && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleStopCompareSide('right')}
|
||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs ${isDark ? 'bg-rose-600/80 hover:bg-rose-600 text-white' : 'bg-red-500 hover:bg-red-600 text-white'
|
||
}`}
|
||
>
|
||
<Square className="w-3 h-3 fill-current" />
|
||
停止
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 min-w-0 overflow-hidden">
|
||
<div className="flex-1 min-w-0">
|
||
{(compare.right.mode === 'single-round' || compare.right.mode === 'single-chat') ? (
|
||
<SingleRagAnswer trace={compare.right.singleRagTrace || parseSingleRagStream([])} onCitationClick={(idx) => handleCompareCitationClick('right', idx)} />
|
||
) : (
|
||
<RetrievalThinkingTrace trace={compare.right.thinkingTrace || createMultiRoundStreamTrace()} onCitationClick={(idx) => handleCompareCitationClick('right', idx)} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{/* 上方结果 */}
|
||
<div className={`min-w-0 overflow-hidden rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<QAAvatar role="assistant" variant="blue" isActive={compare.left.loading} label="知识库助手" />
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||
}`}>
|
||
{getModeLabel(compare.left.mode)}
|
||
</span>
|
||
</div>
|
||
{compare.left.loading && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleStopCompareSide('left')}
|
||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs ${isDark ? 'bg-rose-600/80 hover:bg-rose-600 text-white' : 'bg-red-500 hover:bg-red-600 text-white'
|
||
}`}
|
||
>
|
||
<Square className="w-3 h-3 fill-current" />
|
||
停止
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 min-w-0 overflow-hidden">
|
||
<div className="flex-1 min-w-0">
|
||
{(compare.left.mode === 'single-round' || compare.left.mode === 'single-chat') ? (
|
||
<SingleRagAnswer trace={compare.left.singleRagTrace || parseSingleRagStream([])} onCitationClick={(idx) => handleCompareCitationClick('left', idx)} />
|
||
) : (
|
||
<RetrievalThinkingTrace trace={compare.left.thinkingTrace || createMultiRoundStreamTrace()} onCitationClick={(idx) => handleCompareCitationClick('left', idx)} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 下方结果 */}
|
||
<div className={`min-w-0 overflow-hidden rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<QAAvatar role="assistant" variant="purple" isActive={compare.right.loading} label="知识库助手" />
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||
}`}>
|
||
{getModeLabel(compare.right.mode)}
|
||
</span>
|
||
</div>
|
||
{compare.right.loading && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleStopCompareSide('right')}
|
||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs ${isDark ? 'bg-rose-600/80 hover:bg-rose-600 text-white' : 'bg-red-500 hover:bg-red-600 text-white'
|
||
}`}
|
||
>
|
||
<Square className="w-3 h-3 fill-current" />
|
||
停止
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="p-4 min-w-0 overflow-hidden">
|
||
<div className="flex-1 min-w-0">
|
||
{(compare.right.mode === 'single-round' || compare.right.mode === 'single-chat') ? (
|
||
<SingleRagAnswer trace={compare.right.singleRagTrace || parseSingleRagStream([])} onCitationClick={(idx) => handleCompareCitationClick('right', idx)} />
|
||
) : (
|
||
<RetrievalThinkingTrace trace={compare.right.thinkingTrace || createMultiRoundStreamTrace()} onCitationClick={(idx) => handleCompareCitationClick('right', idx)} />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
) : (
|
||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||
<GitCompare className="w-16 h-16 mb-4 opacity-50" />
|
||
<p className="text-lg">对比模式</p>
|
||
<p className="text-sm mt-2">输入问题,对比不同模式的回答效果</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))
|
||
}
|
||
{showScrollToBottom && (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setIsAutoScrollEnabled(true);
|
||
setShowScrollToBottom(false);
|
||
scrollToBottom(true);
|
||
}}
|
||
className={`fixed left-1/2 -translate-x-1/2 bottom-36 z-20 w-11 h-11 rounded-full backdrop-blur-sm border shadow-lg transition-all ${isDark
|
||
? 'bg-slate-800/65 border-slate-500/60 text-slate-100 hover:bg-slate-700/70'
|
||
: 'bg-white/70 border-gray-300 text-gray-700 hover:bg-white/85'
|
||
}`}
|
||
title="滚动到底部"
|
||
>
|
||
<ArrowDown className="w-5 h-5 mx-auto" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 底部输入区 */}
|
||
<QAComposer
|
||
value={inputValue}
|
||
onChange={setInputValue}
|
||
onKeyDown={handleKeyDown}
|
||
onSubmit={viewMode === 'compare' ? handleCompare : handleSend}
|
||
onStop={handleStop}
|
||
canSubmit={!isLoading && Boolean(inputValue.trim())}
|
||
onReset={handleSafeReset}
|
||
onSave={handleSaveRecord}
|
||
onCollect={handleCollect}
|
||
onOpenSettings={() => setIsModelSettingsOpen(true)}
|
||
isLoading={isLoading}
|
||
placeholder={viewMode === 'compare' ? '输入问题进行对比分析...' : '输入你的问题...'}
|
||
showReset
|
||
/>
|
||
</div>
|
||
|
||
{/* 右侧辅助区 */}
|
||
{viewMode === 'single' && showProcess && (
|
||
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
|
||
}`}>
|
||
{/* 检索过程 */}
|
||
<div className="mb-6">
|
||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||
检索过程
|
||
</h3>
|
||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>当前模式</span>
|
||
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
|
||
{searchProcess.currentMode}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>检索轮次</span>
|
||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||
{searchProcess.rounds}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>命中数</span>
|
||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||
{searchProcess.hits}
|
||
</span>
|
||
</div>
|
||
{searchProcess.queryRewrite && (
|
||
<div>
|
||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>Query改写</span>
|
||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||
{searchProcess.queryRewrite}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 对比模式的差异总结 */}
|
||
{/* {viewMode === 'compare' && compareResult?.differences && (
|
||
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
|
||
}`}>
|
||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||
差异总结
|
||
</h3>
|
||
<div className={`p-4 rounded-xl space-y-4 ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||
<div>
|
||
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-green-400' : 'text-green-600'}`}>
|
||
<CheckCircle className="w-3 h-3" />
|
||
一致观点
|
||
</span>
|
||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||
{compareResult.differences.consensus}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`}>
|
||
<AlertCircle className="w-3 h-3" />
|
||
差异观点
|
||
</span>
|
||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||
{compareResult.differences.differences}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className={`text-xs ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
|
||
{getModeLabel(compareResult.left.mode)}优势
|
||
</span>
|
||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||
{compareResult.differences.leftAdvantage}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className={`text-xs ${isDark ? 'text-purple-400' : 'text-purple-600'}`}>
|
||
{getModeLabel(compareResult.right.mode)}优势
|
||
</span>
|
||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||
{compareResult.differences.rightAdvantage}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)} */}
|
||
</div>
|
||
|
||
{editingMessageId && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div className="absolute inset-0 bg-black/45 backdrop-blur-[1px]" onClick={handleCancelAssistantEdit} />
|
||
<div className={`relative w-full max-w-3xl rounded-2xl border shadow-2xl p-5 ${isDark ? 'bg-slate-900 border-slate-700 text-slate-100' : 'bg-white border-gray-200 text-gray-900'
|
||
}`}>
|
||
<button
|
||
type="button"
|
||
onClick={handleCancelAssistantEdit}
|
||
className={`absolute top-3 right-3 p-1.5 rounded-lg transition-all ${isDark ? 'hover:bg-slate-700 text-slate-300' : 'hover:bg-gray-100 text-gray-500'
|
||
}`}
|
||
title="关闭"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
<div className="pr-9">
|
||
<h3 className="text-base font-semibold">编辑模型生成内容</h3>
|
||
<p className={`mt-1 text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||
可同时修改模型回答和思考过程,多轮检索会把思考过程写回第一轮思考内容。
|
||
</p>
|
||
</div>
|
||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||
<label className="space-y-2">
|
||
<span className={`text-sm font-medium ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>思考过程</span>
|
||
<textarea
|
||
value={editingThinking}
|
||
onChange={(event) => setEditingThinking(event.target.value)}
|
||
className={`h-80 w-full resize-none rounded-xl border p-3 text-sm focus:outline-none ${isDark ? 'border-slate-700 bg-slate-800 text-slate-100' : 'border-gray-200 bg-gray-50 text-gray-800'
|
||
}`}
|
||
/>
|
||
</label>
|
||
<label className="space-y-2">
|
||
<span className={`text-sm font-medium ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>最终回答</span>
|
||
<textarea
|
||
value={editingAnswer}
|
||
onChange={(event) => setEditingAnswer(event.target.value)}
|
||
className={`h-80 w-full resize-none rounded-xl border p-3 text-sm focus:outline-none ${isDark ? 'border-slate-700 bg-slate-800 text-slate-100' : 'border-gray-200 bg-gray-50 text-gray-800'
|
||
}`}
|
||
/>
|
||
</label>
|
||
</div>
|
||
<div className="mt-4 flex justify-end gap-2">
|
||
<button type="button" onClick={handleCancelAssistantEdit} className={`px-4 py-2 rounded-xl text-sm ${isDark ? 'bg-slate-800 hover:bg-slate-700 text-slate-200' : 'bg-gray-100 hover:bg-gray-200 text-gray-700'}`}>
|
||
取消
|
||
</button>
|
||
<button type="button" onClick={handleSaveAssistantEdit} className="px-4 py-2 rounded-xl text-sm bg-red-500 hover:bg-red-600 text-white">
|
||
保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{selectedCitation && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div
|
||
className="absolute inset-0 bg-black/45 backdrop-blur-[1px]"
|
||
onClick={() => setSelectedCitation(null)}
|
||
/>
|
||
<div className={`relative w-full max-w-xl rounded-2xl border shadow-2xl p-5 ${isDark
|
||
? 'bg-slate-900 border-slate-700 text-slate-100'
|
||
: 'bg-white border-gray-200 text-gray-900'
|
||
}`}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setSelectedCitation(null)}
|
||
className={`absolute top-3 right-3 p-1.5 rounded-lg transition-all ${isDark ? 'hover:bg-slate-700 text-slate-300' : 'hover:bg-gray-100 text-gray-500'
|
||
}`}
|
||
title="关闭"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
|
||
<div className="pr-9">
|
||
<h3 className="text-base font-semibold">{selectedCitation.title || '引用内容'}</h3>
|
||
<div className={`mt-2 text-xs space-y-1 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||
<div>来源:{selectedCitation.source || selectedCitation.type || '未知来源'}</div>
|
||
<div>时间:{selectedCitation.time || '未知'}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{selectedCitation.url && (
|
||
<div className="mt-4">
|
||
<a
|
||
href={`${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${selectedCitation.url}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm transition-all ${isDark
|
||
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
||
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
||
}`}
|
||
title="打开原始链接"
|
||
>
|
||
<ExternalLink className="w-4 h-4" />
|
||
查看原文链接
|
||
</a>
|
||
</div>
|
||
)}
|
||
|
||
<div className={`mt-4 rounded-xl p-3 text-sm leading-relaxed max-h-[500px] overflow-auto pr-[10px] whitespace-pre-wrap ${isDark ? 'bg-slate-800/70 text-slate-200' : 'bg-gray-50 text-gray-700'
|
||
}`}>
|
||
{selectedCitation.content || selectedCitation.snippet || '暂无引用内容'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<Dialog
|
||
visible={isModelSettingsOpen}
|
||
header="模型参数设置"
|
||
width="520px"
|
||
confirmBtn="保存"
|
||
cancelBtn="取消"
|
||
onConfirm={() => setIsModelSettingsOpen(false)}
|
||
onClose={() => setIsModelSettingsOpen(false)}
|
||
>
|
||
<div className="space-y-6 py-2">
|
||
<div>
|
||
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||
模型
|
||
</label>
|
||
<Select
|
||
value={modelName}
|
||
onChange={(value) => setModelName(String(value))}
|
||
options={[
|
||
{ label: 'Qwen3-32B: 效果与速度均衡(推荐)', value: 'OpenAI3' },
|
||
{ label: 'Qwen3-235B:效果与速度均衡(推荐)', value: 'OpenAI4' },
|
||
{ label: 'GLM-5:逻辑性强,长上下文', value: 'OpenAI5' },
|
||
{ label: 'Qwen3.5-27B:深度思考过程长', value: 'OpenAI6' },
|
||
{ label: 'Qwen3-8B:速度快,效果较差', value: 'OpenAI7' },
|
||
// { label: 'Qwen2.5-72B:老模型,发散性差', value: 'OpenAI8' },
|
||
{ label: '四季7B:速度快,效果较差', value: 'OpenAI9' },
|
||
]}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<div className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||
相似度阈值 score_threshold:{scoreThreshold.toFixed(2)}
|
||
</div>
|
||
<Slider
|
||
label
|
||
layout="horizontal"
|
||
min={0}
|
||
max={1}
|
||
step={0.01}
|
||
value={scoreThreshold}
|
||
onChange={(value) => setScoreThreshold(Number(value))}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<div className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||
温度 temperature:{temperature.toFixed(2)}
|
||
</div>
|
||
<Slider
|
||
label
|
||
layout="horizontal"
|
||
min={0}
|
||
max={1}
|
||
step={0.01}
|
||
value={temperature}
|
||
onChange={(value) => setTemperature(Number(value))}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ActionChat;
|