Commit 94a7e78b by Liu

fix:kuan

parent dee25152
...@@ -22,6 +22,9 @@ import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react' ...@@ -22,6 +22,9 @@ import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
import { setIsAsking } from '@/store/chatSlice' import { setIsAsking } from '@/store/chatSlice'
import SdreamLoading from '@/components/SdreamLoading' import SdreamLoading from '@/components/SdreamLoading'
// 记录已自动触发提问的会话,避免严格模式或多次渲染造成重复调用
const autoSubmittedConversationIds = new Set<string>()
function formatDateTime(date: Date) { function formatDateTime(date: Date) {
const pad = (num: number) => num.toString().padStart(2, '0') const pad = (num: number) => num.toString().padStart(2, '0')
const year = date.getFullYear() const year = date.getFullYear()
...@@ -80,6 +83,8 @@ export const Chat: React.FC = () => { ...@@ -80,6 +83,8 @@ export const Chat: React.FC = () => {
const [hasHistoryRecord, setHasHistoryRecord] = useState(false) const [hasHistoryRecord, setHasHistoryRecord] = useState(false)
const [historyItemsCount, setHistoryItemsCount] = useState(0) const [historyItemsCount, setHistoryItemsCount] = useState(0)
const [historyTimestamp, setHistoryTimestamp] = useState('') const [historyTimestamp, setHistoryTimestamp] = useState('')
// 当历史记录需要追加到新消息之后时,用于确定分隔线插入位置(分隔线插在 historyDividerIndex 前一个元素之后)
const [historyDividerIndex, setHistoryDividerIndex] = useState<number | null>(null)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { shouldSendQuestion, currentToolId, conversations } = useAppSelector((state: RootState) => state.conversation) const { shouldSendQuestion, currentToolId, conversations } = useAppSelector((state: RootState) => state.conversation)
const scrollableRef = useRef<HTMLDivElement | any>(null) const scrollableRef = useRef<HTMLDivElement | any>(null)
...@@ -95,6 +100,8 @@ export const Chat: React.FC = () => { ...@@ -95,6 +100,8 @@ export const Chat: React.FC = () => {
const isFromTactics = searchParams.get('from') === 'tactics' const isFromTactics = searchParams.get('from') === 'tactics'
const [showClearConfirm, setShowClearConfirm] = useState(false) const [showClearConfirm, setShowClearConfirm] = useState(false)
const [isClearingHistory, setIsClearingHistory] = useState(false) const [isClearingHistory, setIsClearingHistory] = useState(false)
// 是否需要在自动提问完成后再拼接历史记录
const shouldAppendHistoryAfterAutoRef = useRef(false)
// 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时, // 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时,
// 视为一次新的会话入口:重置为通用模式,清除历史遗留的工具模式状态 // 视为一次新的会话入口:重置为通用模式,清除历史遗留的工具模式状态
...@@ -197,9 +204,23 @@ export const Chat: React.FC = () => { ...@@ -197,9 +204,23 @@ export const Chat: React.FC = () => {
/** 处理正常stream的数据 */ /** 处理正常stream的数据 */
const handleStreamMesageData = (msg: any, question: string) => { const handleStreamMesageData = (msg: any, question: string) => {
const resolvedQuestion = question || msg?.content?.data?.question || ''
setAllItems((prevItems) => { setAllItems((prevItems) => {
const newItems = [...prevItems] // 创建数组的浅拷贝 const newItems = [...prevItems] // 创建数组的浅拷贝
const lastIndex = newItems.length - 1 const lastIndex = newItems.length - 1
const prevIndex = lastIndex - 1
// 如果自动提问时后端返回了具体的问题,补全上一条用户问题
if (
resolvedQuestion
&& prevIndex >= 0
&& newItems[prevIndex].role === 'user'
&& !newItems[prevIndex].question
) {
newItems[prevIndex] = {
...newItems[prevIndex],
question: resolvedQuestion,
}
}
if (lastIndex >= 0) { if (lastIndex >= 0) {
// 创建最后一项的新对象,合并现有数据和新的 answer // 创建最后一项的新对象,合并现有数据和新的 answer
const originalAnswer = (newItems[lastIndex].answerList?.[0]?.answer || '') + msg.content.data.answer const originalAnswer = (newItems[lastIndex].answerList?.[0]?.answer || '') + msg.content.data.answer
...@@ -219,8 +240,8 @@ export const Chat: React.FC = () => { ...@@ -219,8 +240,8 @@ export const Chat: React.FC = () => {
], ],
} }
// 只有当question不为空时才设置question字段 // 只有当question不为空时才设置question字段
if (question) { if (resolvedQuestion) {
updatedItem.question = question updatedItem.question = resolvedQuestion
} }
newItems[lastIndex] = updatedItem newItems[lastIndex] = updatedItem
} }
...@@ -230,6 +251,7 @@ export const Chat: React.FC = () => { ...@@ -230,6 +251,7 @@ export const Chat: React.FC = () => {
/** 处理超过最大条数的数据 */ /** 处理超过最大条数的数据 */
const handleChatMaxCount = (msg: any, question: string) => { const handleChatMaxCount = (msg: any, question: string) => {
const resolvedQuestion = question || msg?.content?.data?.question || ''
// toast(t => ( // toast(t => (
// <div className="flex items-center"> // <div className="flex items-center">
// <p className="text-[14px]">⚠️ 超过最大轮数上限!请新建对话 👉🏻</p> // <p className="text-[14px]">⚠️ 超过最大轮数上限!请新建对话 👉🏻</p>
...@@ -275,8 +297,8 @@ export const Chat: React.FC = () => { ...@@ -275,8 +297,8 @@ export const Chat: React.FC = () => {
], ],
} }
// 只有当question不为空时才设置question字段 // 只有当question不为空时才设置question字段
if (question) { if (resolvedQuestion) {
updatedItem.question = question updatedItem.question = resolvedQuestion
} }
newItems[lastIndex] = updatedItem newItems[lastIndex] = updatedItem
} }
...@@ -284,144 +306,20 @@ export const Chat: React.FC = () => { ...@@ -284,144 +306,20 @@ export const Chat: React.FC = () => {
}) })
} }
/** 提交问题 */
const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string, isAutoCall?: boolean) => {
const resolvedToolId = toolId ?? currentToolId ?? undefined
// 根据是否是自动调用设置不同的参数
const busiType = isAutoCall ? '01' : '02'
const recordType = isAutoCall ? 'A02' : 'A01'
// 停止之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const isNew = allItems.length <= 1
dispatch(setIsAsking(true))
// 检查token
await fetchCheckTokenApi()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
if (question) {
// 一次性添加用户问题和空的AI回答
setAllItems(prevItems => [
...prevItems,
{
role: 'user',
question,
} as ChatRecord,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
}
else {
// 自动调用时,只添加空的AI回答
setAllItems(prevItems => [
...prevItems,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController()
let fetchUrl = `/conversation/api/conversation/mobile/v1/submit_question_stream`
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
let proxy = ''
if (viteOutputObj === 'open') {
proxy = import.meta.env.MODE !== 'prod' ? '/api' : '/dev-sdream-api'
}
else {
proxy = import.meta.env.MODE === 'dev' ? '/api' : '/dev-sdream-api'
}
fetchUrl = proxy + fetchUrl
// 构建请求参数,包含路由参数
const requestParams: Record<string, any> = {
question,
conversationId: currentIdRef.current,
stream: true,
productCode,
toolId: resolvedToolId,
busiType,
recordType,
}
// 添加路由参数(如果存在)
if (taskId !== undefined) {
requestParams.taskId = taskId
}
if (version !== undefined) {
requestParams.version = version
}
if (pinBeginTime !== undefined) {
requestParams.pinBeginTime = pinBeginTime
}
if (pinEndTime !== undefined) {
requestParams.pinEndTime = pinEndTime
}
if (partOrAll !== undefined) {
requestParams.partOrAll = partOrAll
}
if (channel !== undefined) {
requestParams.channel = channel
}
if (channelName !== undefined) {
requestParams.channelName = channelName
}
fetchStreamResponse(
fetchUrl,
requestParams,
(msg) => {
// 检查是否已被取消
if (abortControllerRef.current?.signal.aborted) {
return
}
// 处理错误
if (msg?.type === 'ERROR') {
// 如果是 AbortError,不显示错误
if (msg.content?.name === 'AbortError') {
return
}
return
}
// 正常的stream数据
if (msg?.type === 'DATA' && msg?.content?.code === '00000000') {
handleStreamMesageData(msg, question)
}
if (msg?.type === 'DATA' && msg?.content?.code === '01010005') {
handleChatMaxCount(msg, question)
return
}
if (msg.type === 'END') {
if (isNew) {
setTimeout(() => {
dispatch(fetchConversations())
}, 2000)
}
}
},
abortControllerRef.current.signal,
)
}
/** 获取qa记录 */ /** 获取qa记录 */
const getUserQaRecordPage = useCallback(async (conversationId: string) => { const getUserQaRecordPage = useCallback(async (
conversationId: string,
options?: { append?: boolean, showLoading?: boolean },
) => {
const append = options?.append ?? false
const showLoading = options?.showLoading ?? !append
if (showLoading)
setIsLoading(true) setIsLoading(true)
setHasHistoryRecord(false) setHasHistoryRecord(false)
setHistoryItemsCount(0) setHistoryItemsCount(0)
setHistoryTimestamp('') setHistoryTimestamp('')
if (!append)
setHistoryDividerIndex(null)
try { try {
// 检测是否从收藏页返回 // 检测是否从收藏页返回
const fromCollect = location.state?.fromCollect const fromCollect = location.state?.fromCollect
...@@ -447,9 +345,32 @@ export const Chat: React.FC = () => { ...@@ -447,9 +345,32 @@ export const Chat: React.FC = () => {
} }
return item return item
}) })
setAllItems(processedMessages) const messagesWithoutSystemForAppend = append
? processedMessages.filter((_, idx) => !(idx === 0 && processedMessages[idx].role === 'system'))
: processedMessages
if (append) {
if (!messagesWithoutSystemForAppend.length) {
setHistoryDividerIndex(null)
}
setAllItems((prev) => {
const baseLength = prev.length
const historyLength = messagesWithoutSystemForAppend.length
if (historyLength) {
// 分割线放在历史记录之后
setHistoryDividerIndex(baseLength + historyLength - 1)
}
return [
...prev,
...messagesWithoutSystemForAppend,
]
})
}
else {
setAllItems(messagesWithoutSystemForAppend)
}
setHasHistoryRecord(hasQaRecords) setHasHistoryRecord(hasQaRecords)
setHistoryItemsCount(hasQaRecords ? processedMessages.length : 0) setHistoryItemsCount(hasQaRecords ? messagesWithoutSystemForAppend.length : 0)
if (hasQaRecords) { if (hasQaRecords) {
setHistoryTimestamp(formatDateTime(new Date())) setHistoryTimestamp(formatDateTime(new Date()))
} }
...@@ -566,10 +487,151 @@ export const Chat: React.FC = () => { ...@@ -566,10 +487,151 @@ export const Chat: React.FC = () => {
// 错误处理 // 错误处理
} }
finally { finally {
if (showLoading)
setIsLoading(false) setIsLoading(false)
} }
}, [dispatch, currentToolId, conversations, location.state]) }, [dispatch, currentToolId, conversations, location.state])
/** 提交问题 */
const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string, isAutoCall?: boolean) => {
const resolvedToolId = toolId ?? currentToolId ?? undefined
// 根据是否是自动调用设置不同的参数
const busiType = isAutoCall ? '01' : '02'
const recordType = isAutoCall ? 'A02' : 'A01'
// 停止之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const isNew = allItems.length <= 1
dispatch(setIsAsking(true))
// 检查token
await fetchCheckTokenApi()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
// 自动调用也先插入一条用户问题占位,待后端返回真实问题后补全
setAllItems(prevItems => [
...prevItems,
{
role: 'user',
question,
} as ChatRecord,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
// 创建新的 AbortController
abortControllerRef.current = new AbortController()
let fetchUrl = `/conversation/api/conversation/mobile/v1/submit_question_stream`
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
let proxy = ''
if (viteOutputObj === 'open') {
proxy = import.meta.env.MODE !== 'prod' ? '/api' : '/dev-sdream-api'
}
else {
proxy = import.meta.env.MODE === 'dev' ? '/api' : '/dev-sdream-api'
}
fetchUrl = proxy + fetchUrl
// 构建请求参数,包含路由参数
const requestParams: Record<string, any> = {
conversationId: currentIdRef.current,
stream: true,
productCode,
toolId: resolvedToolId,
busiType,
recordType,
}
// 自动调用不传 question;手动调用正常传递
if (!isAutoCall) {
requestParams.question = question
}
// 添加路由参数(如果存在)
if (taskId !== undefined) {
requestParams.taskId = taskId
}
if (version !== undefined) {
requestParams.version = version
}
if (pinBeginTime !== undefined) {
requestParams.pinBeginTime = pinBeginTime
}
if (pinEndTime !== undefined) {
requestParams.pinEndTime = pinEndTime
}
if (partOrAll !== undefined) {
requestParams.partOrAll = partOrAll
}
if (channel !== undefined) {
requestParams.channel = channel
}
if (channelName !== undefined) {
requestParams.channelName = channelName
}
fetchStreamResponse(
fetchUrl,
requestParams,
(msg) => {
// 检查是否已被取消
if (abortControllerRef.current?.signal.aborted) {
return
}
// 处理错误
if (msg?.type === 'ERROR') {
// 如果是 AbortError,不显示错误
if (msg.content?.name === 'AbortError') {
return
}
return
}
// 正常的stream数据
if (msg?.type === 'DATA' && msg?.content?.code === '00000000') {
handleStreamMesageData(msg, question)
}
if (msg?.type === 'DATA' && msg?.content?.code === '01010005') {
handleChatMaxCount(msg, question)
return
}
if (msg.type === 'END') {
if (isNew) {
setTimeout(() => {
dispatch(fetchConversations())
}, 2000)
}
// 自动提问完成后,再追加历史记录
if (isAutoCall && shouldAppendHistoryAfterAutoRef.current && currentIdRef.current) {
shouldAppendHistoryAfterAutoRef.current = false
getUserQaRecordPage(currentIdRef.current, { append: true, showLoading: false })
}
}
},
abortControllerRef.current.signal,
)
}
/**
* 战术入口的自动提问(进入页面和“重新分析”按钮公用)
* 保证每次都按自动模式调用,不携带 question 参数
*/
const triggerAutoSubmit = useCallback(() => {
if (!currentIdRef.current || isLoading)
return
// 自动调用后重新追加最新历史
shouldAppendHistoryAfterAutoRef.current = true
handleSubmitQuestion('', undefined, currentToolId, true)
}, [handleSubmitQuestion, isLoading, currentToolId])
/** 点击滚动到底部 */ /** 点击滚动到底部 */
const scrollToBottom = () => { const scrollToBottom = () => {
scrollableRef.current.scrollTo(scrollableRef.current.scrollHeight, { behavior: 'smooth' }) scrollableRef.current.scrollTo(scrollableRef.current.scrollHeight, { behavior: 'smooth' })
...@@ -586,9 +648,21 @@ export const Chat: React.FC = () => { ...@@ -586,9 +648,21 @@ export const Chat: React.FC = () => {
currentIdRef.current = id currentIdRef.current = id
lastSentQuestionRef.current = '' // 重置标记 lastSentQuestionRef.current = '' // 重置标记
hasAutoSubmittedRef.current = false // 重置自动提交标记 hasAutoSubmittedRef.current = false // 重置自动提交标记
shouldAppendHistoryAfterAutoRef.current = isFromTactics
// 初始化为仅包含 system 的列表,等待自动提问或历史拉取
setAllItems([{ role: 'system' } as ChatRecord])
setHasHistoryRecord(false)
setHistoryItemsCount(0)
setHistoryTimestamp('')
setHistoryDividerIndex(null)
// 非战术入口:立即拉取历史(保持原行为)
if (!isFromTactics) {
getUserQaRecordPage(id) getUserQaRecordPage(id)
} }
}, [id]) }
}, [id, isFromTactics, getUserQaRecordPage, dispatch])
// 处理shouldSendQuestion的变化 - 自动发送问题 // 处理shouldSendQuestion的变化 - 自动发送问题
useEffect(() => { useEffect(() => {
...@@ -608,34 +682,37 @@ export const Chat: React.FC = () => { ...@@ -608,34 +682,37 @@ export const Chat: React.FC = () => {
} }
}, [shouldSendQuestion, isLoading, currentToolId]) }, [shouldSendQuestion, isLoading, currentToolId])
// 页面加载时自动调用submit接口 // 页面加载时自动调用 submit 接口(战术入口只触发一次)
useEffect(() => { useEffect(() => {
if ( if (!isFromTactics)
currentIdRef.current return
&& !isLoading
&& !hasAutoSubmittedRef.current const conversationId = currentIdRef.current
&& isFromTactics // 确保存在会话且未处于加载中
) { if (!conversationId || isLoading)
return
// 避免严格模式或多次渲染导致重复自动提交
if (autoSubmittedConversationIds.has(conversationId))
return
autoSubmittedConversationIds.add(conversationId)
hasAutoSubmittedRef.current = true hasAutoSubmittedRef.current = true
// 确保历史记录加载完成后再调用submit接口 // 确保历史记录加载完成后再调用submit接口
setTimeout(() => { setTimeout(() => {
handleSubmitQuestion('', undefined, currentToolId, true) // 自动调用,传递 isAutoCall = true triggerAutoSubmit() // 自动调用,传递 isAutoCall = true
}, 100) }, 100)
} }, [isLoading, isFromTactics, triggerAutoSubmit])
}, [isLoading, currentToolId, isFromTactics])
// 监听“重新分析”事件,重新发起一次自动提问(仍使用路由参数) // 监听“重新分析”事件,重新发起一次自动提问(仍使用路由参数)
useEffect(() => { useEffect(() => {
const handleReAnalyze = () => { const handleReAnalyze = () => {
if (currentIdRef.current && !isLoading) { triggerAutoSubmit()
handleSubmitQuestion('', undefined, currentToolId, true)
}
} }
window.addEventListener('tacticsReAnalyze', handleReAnalyze as EventListener) window.addEventListener('tacticsReAnalyze', handleReAnalyze as EventListener)
return () => { return () => {
window.removeEventListener('tacticsReAnalyze', handleReAnalyze as EventListener) window.removeEventListener('tacticsReAnalyze', handleReAnalyze as EventListener)
} }
}, [isLoading, currentToolId]) }, [triggerAutoSubmit])
// 根据 currentToolId 获取对应的 toolName // 根据 currentToolId 获取对应的 toolName
useEffect(() => { useEffect(() => {
...@@ -775,9 +852,10 @@ export const Chat: React.FC = () => { ...@@ -775,9 +852,10 @@ export const Chat: React.FC = () => {
const uniqueKey = recordId const uniqueKey = recordId
? `${record.role}-${recordId}` ? `${record.role}-${recordId}`
: `${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}` : `${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}`
const shouldShowHistoryDivider = hasHistoryRecord const shouldShowHistoryDivider = historyDividerIndex !== null
&& hasHistoryRecord
&& historyItemsCount > 0 && historyItemsCount > 0
&& index === historyItemsCount - 1 && index === historyDividerIndex
const historyDividerText = `以上为历史分析数据 ${historyTimestamp || formatDateTime(new Date())}` const historyDividerText = `以上为历史分析数据 ${historyTimestamp || formatDateTime(new Date())}`
return ( return (
<React.Fragment key={uniqueKey}> <React.Fragment key={uniqueKey}>
......
...@@ -81,7 +81,9 @@ export const Home: React.FC = () => { ...@@ -81,7 +81,9 @@ export const Home: React.FC = () => {
} }
// 处理工具按钮点击 // 处理工具按钮点击
const requestIdRef = useRef(0) // 标记最新请求,避免旧响应覆盖
const _handleToolClick = useCallback(async (isToolBtn: boolean, toolId?: string, ignoreUrlToolId?: boolean) => { const _handleToolClick = useCallback(async (isToolBtn: boolean, toolId?: string, ignoreUrlToolId?: boolean) => {
const currentRequestId = ++requestIdRef.current
// 提质增效模式 / 数据助手 / 通用模式:都先清空数据,重新拉常见问题 // 提质增效模式 / 数据助手 / 通用模式:都先清空数据,重新拉常见问题
setOtherQuestions((prev: any) => ({ setOtherQuestions((prev: any) => ({
...prev, ...prev,
...@@ -115,7 +117,8 @@ export const Home: React.FC = () => { ...@@ -115,7 +117,8 @@ export const Home: React.FC = () => {
const res = await fetchEfficiencyQuestionList({ const res = await fetchEfficiencyQuestionList({
toolId: sessionToolId || finalToolId, toolId: sessionToolId || finalToolId,
}) })
if (res && res.data && res.data.questions) { // 只接受当前最新的请求结果,避免旧请求覆盖新请求
if (currentRequestId === requestIdRef.current && res?.data?.questions) {
setOtherQuestions((prev: any) => ({ setOtherQuestions((prev: any) => ({
...prev, ...prev,
content: res.data.questions || [], content: res.data.questions || [],
...@@ -126,7 +129,9 @@ export const Home: React.FC = () => { ...@@ -126,7 +129,9 @@ export const Home: React.FC = () => {
console.error('获取工具相关问题失败:', error) console.error('获取工具相关问题失败:', error)
} }
finally { finally {
setIsDataLoaded(true) // 无论成功失败都标记为已加载 // 仅在当前请求仍是最新时更新加载态,避免闪烁
if (currentRequestId === requestIdRef.current)
setIsDataLoaded(true)
} }
}, [originalOtherQuestions, location.search]) }, [originalOtherQuestions, location.search])
...@@ -329,7 +334,7 @@ export const Home: React.FC = () => { ...@@ -329,7 +334,7 @@ export const Home: React.FC = () => {
<div className="hidden sm:flex h-full flex-none ml-auto"> <div className="hidden sm:flex h-full flex-none ml-auto">
<div <div
className="w-full h-full bg-transparent box-border rounded-[24px]" className="w-full h-full bg-transparent box-border rounded-[24px]"
style={{ width: '420px', height: 'calc(100vh - 64px)', background: '#FFFFFF', padding: '0 30px' }} style={{ height: 'calc(100vh - 64px)', background: '#FFFFFF', padding: '0 30px' }}
> >
<Outlet /> <Outlet />
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment