Commit dee25152 by Liu

fix:进入时调用提问接口&&重新分析&&清除记录

parent 8a2d3591
...@@ -41,3 +41,25 @@ ...@@ -41,3 +41,25 @@
width: 100%; width: 100%;
padding-bottom: 32px; padding-bottom: 32px;
} }
.historyDivider {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
color: #9ea3b1;
font-size: 12px;
line-height: 18px;
margin: 20px 0;
}
.historyDividerLine {
flex: 1;
height: 1px;
background: #e5e7ed;
}
.historyDividerText {
white-space: nowrap;
}
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
[key: string]: string
chatPage: string chatPage: string
content: string content: string
historyDivider: string
historyDividerLine: string
historyDividerText: string
inter: string inter: string
scrollView: string scrollView: string
scrollable: string scrollable: string
......
...@@ -12,7 +12,7 @@ import { ChatEditor } from '@/components/ChatEditorTactics' ...@@ -12,7 +12,7 @@ import { ChatEditor } from '@/components/ChatEditorTactics'
import type { ChatRecord } from '@/types/chat' import type { ChatRecord } from '@/types/chat'
import { fetchUserQaRecordPage } from '@/api/conversation' import { fetchUserQaRecordPage } from '@/api/conversation'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat' import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
import { fetchEfficiencyQuestionList, fetchToolList } from '@/api/home' import { deleteUserConversation, fetchEfficiencyQuestionList, fetchToolList } from '@/api/home'
// import { mockFetchToolList } from '@/api/mock/home' // import { mockFetchToolList } from '@/api/mock/home'
import { clearCurrentToolId, clearShouldSendQuestion, fetchConversations, setCurrentToolId } from '@/store/conversationSlice' import { clearCurrentToolId, clearShouldSendQuestion, fetchConversations, setCurrentToolId } from '@/store/conversationSlice'
import { getUserRolesForApi } from '@/lib/utils' import { getUserRolesForApi } from '@/lib/utils'
...@@ -22,6 +22,17 @@ import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react' ...@@ -22,6 +22,17 @@ 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'
function formatDateTime(date: Date) {
const pad = (num: number) => num.toString().padStart(2, '0')
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export const Chat: React.FC = () => { export const Chat: React.FC = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const location = useLocation() const location = useLocation()
...@@ -43,8 +54,32 @@ export const Chat: React.FC = () => { ...@@ -43,8 +54,32 @@ export const Chat: React.FC = () => {
fromState: toolIdFromState, fromState: toolIdFromState,
final: initialToolId, final: initialToolId,
}) })
// 从 URL 获取策略相关参数
const taskId = searchParams.get('taskId') ? Number(searchParams.get('taskId')) : undefined
const version = searchParams.get('version') ? Number(searchParams.get('version')) : undefined
const pinBeginTime = searchParams.get('pinBeginTime') ? Number(searchParams.get('pinBeginTime')) : undefined
const pinEndTime = searchParams.get('pinEndTime') ? Number(searchParams.get('pinEndTime')) : undefined
const partOrAll = searchParams.get('partOrAll') || undefined
const channel = searchParams.get('channel') || undefined
const channelName = searchParams.get('channelName') || undefined
// 调试日志:输出获取到的策略参数
// eslint-disable-next-line no-console
console.log('[Chat] 策略参数:', {
taskId,
version,
pinBeginTime,
pinEndTime,
partOrAll,
channel,
channelName,
})
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [allItems, setAllItems] = useState<ChatRecord[]>([]) const [allItems, setAllItems] = useState<ChatRecord[]>([])
const [hasHistoryRecord, setHasHistoryRecord] = useState(false)
const [historyItemsCount, setHistoryItemsCount] = useState(0)
const [historyTimestamp, setHistoryTimestamp] = useState('')
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)
...@@ -55,7 +90,11 @@ export const Chat: React.FC = () => { ...@@ -55,7 +90,11 @@ export const Chat: React.FC = () => {
const [currentToolName, setCurrentToolName] = useState<string | undefined>(undefined) const [currentToolName, setCurrentToolName] = useState<string | undefined>(undefined)
// 使用 ref 保存从 location.state 传递的 toolId,避免被异步操作覆盖 // 使用 ref 保存从 location.state 传递的 toolId,避免被异步操作覆盖
const toolIdFromStateRef = useRef<string | null | undefined>(undefined) const toolIdFromStateRef = useRef<string | null | undefined>(undefined)
// 使用 ref 跟踪是否已经自动调用过submit接口,避免重复调用
const hasAutoSubmittedRef = useRef<boolean>(false)
const isFromTactics = searchParams.get('from') === 'tactics' const isFromTactics = searchParams.get('from') === 'tactics'
const [showClearConfirm, setShowClearConfirm] = useState(false)
const [isClearingHistory, setIsClearingHistory] = useState(false)
// 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时, // 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时,
// 视为一次新的会话入口:重置为通用模式,清除历史遗留的工具模式状态 // 视为一次新的会话入口:重置为通用模式,清除历史遗留的工具模式状态
...@@ -169,9 +208,8 @@ export const Chat: React.FC = () => { ...@@ -169,9 +208,8 @@ export const Chat: React.FC = () => {
// 去除 [参考文档《任意内容》 《任意内容》...] 格式的内容 // 去除 [参考文档《任意内容》 《任意内容》...] 格式的内容
filteredAnswer = filteredAnswer.replace(/\[参考文档(?:[^]*》\s*)+\]/g, '').trim() filteredAnswer = filteredAnswer.replace(/\[参考文档(?:[^]*》\s*)+\]/g, '').trim()
newItems[lastIndex] = { const updatedItem: ChatRecord = {
...newItems[lastIndex], ...newItems[lastIndex],
question,
answerList: [ answerList: [
{ {
...msg.content.data, ...msg.content.data,
...@@ -180,6 +218,11 @@ export const Chat: React.FC = () => { ...@@ -180,6 +218,11 @@ export const Chat: React.FC = () => {
}, },
], ],
} }
// 只有当question不为空时才设置question字段
if (question) {
updatedItem.question = question
}
newItems[lastIndex] = updatedItem
} }
return newItems return newItems
}) })
...@@ -219,9 +262,8 @@ export const Chat: React.FC = () => { ...@@ -219,9 +262,8 @@ export const Chat: React.FC = () => {
const lastIndex = newItems.length - 1 const lastIndex = newItems.length - 1
if (lastIndex >= 0) { if (lastIndex >= 0) {
// 创建最后一项的新对象,合并现有数据和新的 answer // 创建最后一项的新对象,合并现有数据和新的 answer
newItems[lastIndex] = { const updatedItem: ChatRecord = {
...newItems[lastIndex], ...newItems[lastIndex],
question,
answerList: [ answerList: [
{ {
...msg.content.data, ...msg.content.data,
...@@ -232,16 +274,22 @@ export const Chat: React.FC = () => { ...@@ -232,16 +274,22 @@ export const Chat: React.FC = () => {
}, },
], ],
} }
// 只有当question不为空时才设置question字段
if (question) {
updatedItem.question = question
}
newItems[lastIndex] = updatedItem
} }
return newItems return newItems
}) })
} }
/** 提交问题 */ /** 提交问题 */
const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string) => { const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string, isAutoCall?: boolean) => {
const resolvedToolId = toolId ?? currentToolId ?? undefined const resolvedToolId = toolId ?? currentToolId ?? undefined
const busiType = '02' // 根据是否是自动调用设置不同的参数
const recordType = 'A02' const busiType = isAutoCall ? '01' : '02'
const recordType = isAutoCall ? 'A02' : 'A01'
// 停止之前的请求 // 停止之前的请求
if (abortControllerRef.current) { if (abortControllerRef.current) {
abortControllerRef.current.abort() abortControllerRef.current.abort()
...@@ -253,6 +301,8 @@ export const Chat: React.FC = () => { ...@@ -253,6 +301,8 @@ export const Chat: React.FC = () => {
// 检查token // 检查token
await fetchCheckTokenApi() await fetchCheckTokenApi()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
if (question) {
// 一次性添加用户问题和空的AI回答 // 一次性添加用户问题和空的AI回答
setAllItems(prevItems => [ setAllItems(prevItems => [
...prevItems, ...prevItems,
...@@ -265,6 +315,17 @@ export const Chat: React.FC = () => { ...@@ -265,6 +315,17 @@ export const Chat: React.FC = () => {
answerList: [{ answer: '' }], answerList: [{ answer: '' }],
} as ChatRecord, } as ChatRecord,
]) ])
}
else {
// 自动调用时,只添加空的AI回答
setAllItems(prevItems => [
...prevItems,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
}
// 创建新的 AbortController // 创建新的 AbortController
abortControllerRef.current = new AbortController() abortControllerRef.current = new AbortController()
...@@ -283,9 +344,8 @@ export const Chat: React.FC = () => { ...@@ -283,9 +344,8 @@ export const Chat: React.FC = () => {
fetchUrl = proxy + fetchUrl fetchUrl = proxy + fetchUrl
fetchStreamResponse( // 构建请求参数,包含路由参数
fetchUrl, const requestParams: Record<string, any> = {
{
question, question,
conversationId: currentIdRef.current, conversationId: currentIdRef.current,
stream: true, stream: true,
...@@ -293,7 +353,34 @@ export const Chat: React.FC = () => { ...@@ -293,7 +353,34 @@ export const Chat: React.FC = () => {
toolId: resolvedToolId, toolId: resolvedToolId,
busiType, busiType,
recordType, 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) => { (msg) => {
// 检查是否已被取消 // 检查是否已被取消
if (abortControllerRef.current?.signal.aborted) { if (abortControllerRef.current?.signal.aborted) {
...@@ -332,6 +419,9 @@ export const Chat: React.FC = () => { ...@@ -332,6 +419,9 @@ export const Chat: React.FC = () => {
/** 获取qa记录 */ /** 获取qa记录 */
const getUserQaRecordPage = useCallback(async (conversationId: string) => { const getUserQaRecordPage = useCallback(async (conversationId: string) => {
setIsLoading(true) setIsLoading(true)
setHasHistoryRecord(false)
setHistoryItemsCount(0)
setHistoryTimestamp('')
try { try {
// 检测是否从收藏页返回 // 检测是否从收藏页返回
const fromCollect = location.state?.fromCollect const fromCollect = location.state?.fromCollect
...@@ -339,6 +429,7 @@ export const Chat: React.FC = () => { ...@@ -339,6 +429,7 @@ export const Chat: React.FC = () => {
console.log('[Chat] 开始获取历史记录:', conversationId) console.log('[Chat] 开始获取历史记录:', conversationId)
const res = await fetchUserQaRecordPage(conversationId) const res = await fetchUserQaRecordPage(conversationId)
const qaRecords = res.data || [] const qaRecords = res.data || []
const hasQaRecords = qaRecords.length > 0
const messages = [{ role: 'system' } as ChatRecord, ...processApiResponse(qaRecords)] const messages = [{ role: 'system' } as ChatRecord, ...processApiResponse(qaRecords)]
// 处理历史记录中的参考文档标记 // 处理历史记录中的参考文档标记
const processedMessages = messages.map((item) => { const processedMessages = messages.map((item) => {
...@@ -357,9 +448,13 @@ export const Chat: React.FC = () => { ...@@ -357,9 +448,13 @@ export const Chat: React.FC = () => {
return item return item
}) })
setAllItems(processedMessages) setAllItems(processedMessages)
setHasHistoryRecord(hasQaRecords)
setHistoryItemsCount(hasQaRecords ? processedMessages.length : 0)
if (hasQaRecords) {
setHistoryTimestamp(formatDateTime(new Date()))
}
// 优先从 qaRecords 中查找 toolId(这是实际使用的) // 优先从 qaRecords 中查找 toolId(这是实际使用的)
const latestToolId = [...qaRecords].reverse().find(item => Boolean(item.toolId))?.toolId?.trim?.() const latestToolId = [...qaRecords].reverse().find(item => Boolean(item.toolId))?.toolId?.trim?.()
const hasQaRecords = qaRecords.length > 0
// 如果 qaRecords 中没有 toolId,尝试从 conversations 中查找当前会话的 toolId(会话级别) // 如果 qaRecords 中没有 toolId,尝试从 conversations 中查找当前会话的 toolId(会话级别)
const conversationToolId = latestToolId || (conversations.find(conv => conv.conversationId === conversationId)?.toolId?.trim?.()) const conversationToolId = latestToolId || (conversations.find(conv => conv.conversationId === conversationId)?.toolId?.trim?.())
...@@ -490,6 +585,7 @@ export const Chat: React.FC = () => { ...@@ -490,6 +585,7 @@ export const Chat: React.FC = () => {
currentIdRef.current = id currentIdRef.current = id
lastSentQuestionRef.current = '' // 重置标记 lastSentQuestionRef.current = '' // 重置标记
hasAutoSubmittedRef.current = false // 重置自动提交标记
getUserQaRecordPage(id) getUserQaRecordPage(id)
} }
}, [id]) }, [id])
...@@ -512,6 +608,35 @@ export const Chat: React.FC = () => { ...@@ -512,6 +608,35 @@ export const Chat: React.FC = () => {
} }
}, [shouldSendQuestion, isLoading, currentToolId]) }, [shouldSendQuestion, isLoading, currentToolId])
// 页面加载时自动调用submit接口
useEffect(() => {
if (
currentIdRef.current
&& !isLoading
&& !hasAutoSubmittedRef.current
&& isFromTactics
) {
hasAutoSubmittedRef.current = true
// 确保历史记录加载完成后再调用submit接口
setTimeout(() => {
handleSubmitQuestion('', undefined, currentToolId, true) // 自动调用,传递 isAutoCall = true
}, 100)
}
}, [isLoading, currentToolId, isFromTactics])
// 监听“重新分析”事件,重新发起一次自动提问(仍使用路由参数)
useEffect(() => {
const handleReAnalyze = () => {
if (currentIdRef.current && !isLoading) {
handleSubmitQuestion('', undefined, currentToolId, true)
}
}
window.addEventListener('tacticsReAnalyze', handleReAnalyze as EventListener)
return () => {
window.removeEventListener('tacticsReAnalyze', handleReAnalyze as EventListener)
}
}, [isLoading, currentToolId])
// 根据 currentToolId 获取对应的 toolName // 根据 currentToolId 获取对应的 toolName
useEffect(() => { useEffect(() => {
const getToolNameFromToolId = async () => { const getToolNameFromToolId = async () => {
...@@ -565,11 +690,68 @@ export const Chat: React.FC = () => { ...@@ -565,11 +690,68 @@ export const Chat: React.FC = () => {
} }
}, [dispatch]) }, [dispatch])
const handleConfirmClearHistory = useCallback(async () => {
if (!id)
return
try {
setIsClearingHistory(true)
await deleteUserConversation({ conversationIdList: [id] })
// 通知其他监听方
window.dispatchEvent(new CustomEvent('tacticsClearHistoryConfirm'))
// 本地同步清空
setAllItems([{ role: 'system' } as ChatRecord])
setHasHistoryRecord(false)
setHistoryItemsCount(0)
setHistoryTimestamp('')
}
catch (error) {
console.error('清空历史记录失败:', error)
}
finally {
setShowClearConfirm(false)
setIsClearingHistory(false)
}
}, [id])
// 监听“清除记录”事件,在顶部展示确认弹窗(无遮罩)
useEffect(() => {
const handleShowClearConfirm = () => setShowClearConfirm(true)
window.addEventListener('tacticsClearHistory', handleShowClearConfirm as EventListener)
return () => {
window.removeEventListener('tacticsClearHistory', handleShowClearConfirm as EventListener)
}
}, [])
return ( return (
<div className={styles.scrollView}> <div className={styles.scrollView}>
<div className={`${styles.chatPage} relative`}> <div className={`${styles.chatPage} relative`}>
{/* <ChatSlogan /> {/* <ChatSlogan />
<ChatMaskBar /> */} <ChatMaskBar /> */}
{showClearConfirm && (
<div className="absolute left-1/2 top-[64px] z-[120] w-[340px] -translate-x-1/2">
<div className="rounded-[12px] border border-[#E5E6EB] bg-white shadow-md p-4">
<div className="text-[16px] font-medium text-[#1D2129] mb-2">是否确定清空历史记录?</div>
<div className="flex justify-end gap-3 mt-4">
<Button
size="sm"
variant="bordered"
className="text-[#1D2129] border-[#E5E6EB]"
onPress={() => setShowClearConfirm(false)}
>
取消
</Button>
<Button
size="sm"
color="primary"
isLoading={isClearingHistory}
onPress={handleConfirmClearHistory}
>
确定
</Button>
</div>
</div>
</div>
)}
<div className={`${styles.content}`}> <div className={`${styles.content}`}>
{isLoading && ( {isLoading && (
<div className="w-full h-full flex justify-center items-center"> <div className="w-full h-full flex justify-center items-center">
...@@ -593,10 +775,14 @@ export const Chat: React.FC = () => { ...@@ -593,10 +775,14 @@ 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
&& historyItemsCount > 0
&& index === historyItemsCount - 1
const historyDividerText = `以上为历史分析数据 ${historyTimestamp || formatDateTime(new Date())}`
return ( return (
<React.Fragment key={uniqueKey}>
<div <div
className="w-full chatItem mx-auto" className="w-full chatItem mx-auto"
key={uniqueKey}
> >
{record.role === 'system' && <ChatWelcome toolName={currentToolName} />} {record.role === 'system' && <ChatWelcome toolName={currentToolName} />}
{record.role === 'user' && <ChatItemUser record={record} />} {record.role === 'user' && <ChatItemUser record={record} />}
...@@ -610,6 +796,14 @@ export const Chat: React.FC = () => { ...@@ -610,6 +796,14 @@ export const Chat: React.FC = () => {
/> />
)} )}
</div> </div>
{shouldShowHistoryDivider && (
<div className={styles.historyDivider}>
<div className={styles.historyDividerLine} />
<span className={styles.historyDividerText}>{historyDividerText}</span>
<div className={styles.historyDividerLine} />
</div>
)}
</React.Fragment>
) )
})} })}
</div> </div>
......
...@@ -22,7 +22,7 @@ export const ChatWelcome: React.FC<ChatWelcomeProps> = ({ toolName: _toolName }) ...@@ -22,7 +22,7 @@ export const ChatWelcome: React.FC<ChatWelcomeProps> = ({ toolName: _toolName })
return 'HI~我是您的提质增效助手,有什么可以帮您?' return 'HI~我是您的提质增效助手,有什么可以帮您?'
} }
return '您好,有什么我可以帮您的吗?' return '正在为您分析策略,请耐心等待一会儿哦~'
} }
return ( return (
......
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