Commit 0cfabedf by Liu

更新聊天、首页和导航栏相关功能

parent 552559f6
/*
import { subDays, subMinutes } from 'date-fns'
import type { Conversation } from '@/types/conversation'
......@@ -89,7 +88,7 @@ const conversationRecords: Conversation[] = [
}),
]
const conversationPageMockResponse: ConversationPageResponse = {
const _conversationPageMockResponse: ConversationPageResponse = {
code: '00000000',
message: '成功',
data: {
......@@ -102,9 +101,6 @@ const conversationPageMockResponse: ConversationPageResponse = {
ok: true,
}
export function mockFetchConversationPage() {
return Promise.resolve(conversationPageMockResponse)
}
*/
export {}
// export function mockFetchConversationPage() {
// return Promise.resolve(conversationPageMockResponse)
// }
/*
interface EfficiencyQuestionResponse {
code: string
message: string
......@@ -8,7 +7,7 @@ interface EfficiencyQuestionResponse {
ok: boolean
}
const efficiencyQuestionMockResponse: EfficiencyQuestionResponse = {
const _efficiencyQuestionMockResponse: EfficiencyQuestionResponse = {
code: '00000000',
message: '成功',
data: {
......@@ -26,9 +25,60 @@ const efficiencyQuestionMockResponse: EfficiencyQuestionResponse = {
ok: true,
}
export function mockFetchEfficiencyQuestionList(): Promise<EfficiencyQuestionResponse> {
return Promise.resolve(efficiencyQuestionMockResponse)
// export function mockFetchEfficiencyQuestionList(): Promise<EfficiencyQuestionResponse> {
// return Promise.resolve(efficiencyQuestionMockResponse)
// }
interface ToolListResponse {
code: string
message: string
data: Array<{
toolId: string
toolName: string
toolContent: string
toolIcon: string
toolType: string
userRole: string
showOrder: number
}>
ok: boolean
}
const _toolListMockResponse: ToolListResponse = {
code: '00000000',
message: '成功',
data: [
{
toolId: '6712395743241',
toolName: '提质增效',
toolContent: 'https://sit-wechat.guominpension.com/underwrite',
toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-increase.svg',
toolType: '03',
userRole: '02',
showOrder: 8,
},
{
toolId: '6712395743240',
toolName: '数据助手',
toolContent: 'https://sit-wechat.guominpension.com/underwrite',
toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-data.svg',
toolType: '03',
userRole: '01',
showOrder: 8,
},
{
toolId: 'general-mode',
toolName: '通用模式',
toolContent: 'https://sit-wechat.guominpension.com/underwrite',
toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-normal.svg',
toolType: '01',
userRole: '00',
showOrder: 8,
},
],
ok: true,
}
*/
export {}
// export function mockFetchToolList(): Promise<ToolListResponse> {
// return Promise.resolve(toolListMockResponse)
// }
......@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { Button, Tooltip } from '@heroui/react'
import { useLocalStorageState, useToggle } from 'ahooks'
import { useSearchParams } from 'react-router-dom'
import { LoginModal } from '../LoginModal'
import type { RootState } from '@/store'
import SendIcon from '@/assets/svg/send.svg?react'
......@@ -11,37 +12,35 @@ import { fetchToolList } from '@/api/home'
import { clearCurrentToolId, createConversation, setCurrentToolId } from '@/store/conversationSlice'
import { getUserRolesForApi } from '@/lib/utils'
/*
const MOCK_TOOL_LIST = [
{
toolId: '6712395743241',
toolName: '提质增效',
toolContent: 'https://sit-wechat.guominpension.com/underwrite',
toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-increase.svg',
toolType: '03',
userRole: '02',
showOrder: 8,
},
{
toolId: '6712395743240',
toolName: '数据助手',
toolContent: 'https://sit-wechat.guominpension.com/underwrite',
toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-data.svg',
toolType: '03',
userRole: '01',
showOrder: 8,
},
{
toolId: 'general-mode',
toolName: '通用模式',
toolContent: 'https://sit-wechat.guominpension.com/underwrite',
toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-normal.svg',
toolType: '01',
userRole: '00',
showOrder: 8,
},
] as const
*/
// const MOCK_TOOL_LIST = [
// {
// toolId: '6712395743241',
// toolName: '提质增效',
// toolContent: 'https://sit-wechat.guominpension.com/underwrite',
// toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-increase.svg',
// toolType: '03',
// userRole: '02',
// showOrder: 8,
// },
// {
// toolId: '6712395743240',
// toolName: '数据助手',
// toolContent: 'https://sit-wechat.guominpension.com/underwrite',
// toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-data.svg',
// toolType: '03',
// userRole: '01',
// showOrder: 8,
// },
// {
// toolId: 'general-mode',
// toolName: '通用模式',
// toolContent: 'https://sit-wechat.guominpension.com/underwrite',
// toolIcon: 'http://p-cf-co-1255000025.cos.bj.csyun001.ccbcos.co/tool-normal.svg',
// toolType: '01',
// userRole: '00',
// showOrder: 8,
// },
// ] as const
interface ChatEditorProps {
onChange?: (value: string) => void
......@@ -68,11 +67,15 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const currentToolId = useAppSelector((state: RootState) => state.conversation.currentToolId)
const [showToolQuestion, setShowToolQuestion] = useState<boolean>(false)
const [sessionToolId, setSessionToolId] = useState<string | null>(null)
const [searchParams, setSearchParams] = useSearchParams()
const toolIdFromUrl = searchParams.get('toolId')
// 获取工具列表
const getToolList = async () => {
try {
// 使用统一的方法获取 userRoles(先同步路由到 localStorage,然后读取)
// 使用 mock 数据(已注释)
// setToolList([...MOCK_TOOL_LIST])
// 真实API调用
const userRoles = getUserRolesForApi()
const res = await fetchToolList({ userRoles })
if (res?.data) {
......@@ -98,28 +101,30 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
console.log('[ChatEditor] currentToolId 或 sessionToolId 变化:', {
currentToolId,
sessionToolId,
toolIdFromUrl,
})
if (currentToolId) {
// eslint-disable-next-line no-console
console.log('[ChatEditor] 高亮工具按钮:', currentToolId)
setSelectedToolId(currentToolId)
setIsToolBtnActive(false)
return
}
if (!currentToolId && sessionToolId) {
// eslint-disable-next-line no-console
console.log('[ChatEditor] 使用 sessionToolId 恢复工具高亮:', sessionToolId)
if (sessionToolId) {
setSelectedToolId(sessionToolId)
setIsToolBtnActive(false)
return
}
// 如果 currentToolId 和 sessionToolId 都没有值,根据路由中的 toolId 来决定
if (toolIdFromUrl) {
setSelectedToolId(toolIdFromUrl)
setIsToolBtnActive(false)
}
else {
// eslint-disable-next-line no-console
console.log('[ChatEditor] 没有 currentToolId,回到通用模式')
setSelectedToolId(null)
setIsToolBtnActive(true)
}
}, [currentToolId, sessionToolId])
}, [currentToolId, sessionToolId, toolIdFromUrl])
// 监听 sessionStorage 中的 currentToolId(历史点击时写入)来辅助高亮逻辑
useEffect(() => {
......@@ -134,6 +139,12 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
}
}, [])
// 当路由变化时,同步更新 sessionToolId(因为 storage 事件不会在同标签页触发)
useEffect(() => {
const storedToolId = sessionStorage.getItem('currentToolId')
setSessionToolId(storedToolId)
}, [toolIdFromUrl])
const startAnimation = () => {
intervalRef.current = setInterval(() => {
setCurrentPlaceholder(prev => (prev + 1) % placeholders.length)
......@@ -215,6 +226,12 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
sessionStorage.removeItem('currentToolId')
setSessionToolId(null)
setShowToolQuestion(false)
// 清空路由中的 toolId 参数
if (toolIdFromUrl) {
const newSearchParams = new URLSearchParams(searchParams)
newSearchParams.delete('toolId')
setSearchParams(newSearchParams, { replace: true })
}
try {
await dispatch(createConversation({
conversationData: {},
......@@ -382,8 +399,15 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
{toolList && toolList.length > 0 && (
<div className="absolute left-4 bottom-2 flex items-center gap-3 pointer-events-auto pl-[16px]">
{toolList.map((tool: any, index: number) => {
// tool.toolName === '通用模式' 的按钮(通用模式)在默认状态下高亮
const isSelected = (selectedToolId === tool.toolId) || (tool.toolName === '通用模式' && isToolBtnActive)
// 根据 selectedToolId 或路由中的 toolId 进行匹配,注意数据类型统一转换为字符串比较
// 优先使用 selectedToolId(用户点击后的状态),其次使用 sessionStorage 中的 currentToolId(刷新时使用),再次使用路由中的 toolId(初始化时使用)
const toolIdStr = String(tool.toolId)
const isSelectedByState = selectedToolId && toolIdStr === String(selectedToolId)
const isSelectedBySession = !selectedToolId && sessionToolId && toolIdStr === String(sessionToolId)
const isSelectedByUrl = !selectedToolId && !sessionToolId && toolIdFromUrl && toolIdStr === String(toolIdFromUrl)
// 通用模式高亮:路由内没有 toolId 或 toolId 为空时默认高亮,点击后也要高亮
const isGeneralMode = tool.toolName === '通用模式' && isToolBtnActive && !selectedToolId && !sessionToolId && !toolIdFromUrl
const isSelected = isSelectedByState || isSelectedBySession || isSelectedByUrl || isGeneralMode
// 调试打印
if (index === 0 || selectedToolId === tool.toolId) {
// eslint-disable-next-line no-console
......
......@@ -32,21 +32,21 @@ export const HistoryBarList: React.FC<HistoryBarListProps> = ({ searchValue, onS
toolId: conversation.toolId,
})
if (conversation.toolId) {
// 将当前会话的 toolId 写入 sessionStorage,供 ChatEditor 使用
sessionStorage.setItem('currentToolId', conversation.toolId)
// eslint-disable-next-line no-console
console.log('889999999999:', conversation.toolId)
dispatch(setCurrentToolId(conversation.toolId))
}
else {
// 没有 toolId 时,移除 session 中的记录
sessionStorage.removeItem('currentToolId')
// eslint-disable-next-line no-console
console.log('[HistoryBarList] 清除 toolId')
dispatch(clearCurrentToolId())
}
// 直接导航到历史记录,不设置shouldSendQuestion
navigate(`/chat/${conversation.conversationId}`, {
// 在 URL 中拼接 toolId 作为查询参数,以便刷新页面后仍能保留
const url = conversation.toolId
? `/chat/${conversation.conversationId}?toolId=${conversation.toolId}`
: `/chat/${conversation.conversationId}`
navigate(url, {
state: {
toolId: conversation.toolId || null,
},
......
......@@ -22,7 +22,7 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
const dispatch = useAppDispatch()
const navigate = useNavigate()
const { currentConversationId, shouldNavigateToNewConversation } = useAppSelector(state => state.conversation)
const { currentConversationId, shouldNavigateToNewConversation, currentToolId } = useAppSelector(state => state.conversation)
const handleCreateConversation = () => {
dispatch(createConversation({
......@@ -103,10 +103,14 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
useEffect(() => {
if (shouldNavigateToNewConversation && currentConversationId) {
navigate(`/chat/${currentConversationId}`)
// 如果当前有选中的 toolId,在 URL 中拼接,以便刷新页面后保留工具选择
const url = currentToolId
? `/chat/${currentConversationId}?toolId=${currentToolId}`
: `/chat/${currentConversationId}`
navigate(url)
dispatch(clearNavigationFlag())
}
}, [shouldNavigateToNewConversation, currentConversationId, navigate, dispatch])
}, [shouldNavigateToNewConversation, currentConversationId, currentToolId, navigate, dispatch])
// keep latest conversation id in sessionStorage for cross-page returns (e.g., from collect)
useEffect(() => {
......
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useLocation, useParams } from 'react-router-dom'
import { useLocation, useParams, useSearchParams } from 'react-router-dom'
import { Button } from '@heroui/react'
import { motion } from 'framer-motion'
import { useScroll } from 'ahooks'
......@@ -13,6 +13,7 @@ import type { ChatRecord } from '@/types/chat'
import { fetchUserQaRecordPage } from '@/api/conversation'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
import { fetchToolList } from '@/api/home'
// import { mockFetchToolList } from '@/api/mock/home'
import { clearCurrentToolId, clearShouldSendQuestion, fetchConversations, setCurrentToolId } from '@/store/conversationSlice'
import { getUserRolesForApi } from '@/lib/utils'
import type { RootState } from '@/store'
......@@ -24,7 +25,21 @@ import SdreamLoading from '@/components/SdreamLoading'
export const Chat: React.FC = () => {
const { id } = useParams<{ id: string }>()
const location = useLocation()
const initialToolId = (location.state as { toolId?: string | null } | null)?.toolId
const [searchParams] = useSearchParams()
// 优先从 URL 查询参数读取 toolId(刷新后仍能保留),其次从 location.state 读取
const toolIdFromUrl = searchParams.get('toolId')
// 添加调试日志,查看 location.state 的实际值
// eslint-disable-next-line no-console
console.log('[Chat] location.state:', location.state)
const toolIdFromState = (location.state as { toolId?: string | null } | null)?.toolId
// 优先使用 URL 中的 toolId,其次使用 state 中的 toolId
const initialToolId = toolIdFromUrl !== null ? toolIdFromUrl : toolIdFromState
// eslint-disable-next-line no-console
console.log('[Chat] initialToolId:', {
fromUrl: toolIdFromUrl,
fromState: toolIdFromState,
final: initialToolId,
})
const [isLoading, setIsLoading] = useState(false)
const [allItems, setAllItems] = useState<ChatRecord[]>([])
const dispatch = useAppDispatch()
......@@ -35,13 +50,26 @@ export const Chat: React.FC = () => {
const lastSentQuestionRef = useRef<string>('')
const abortControllerRef = useRef<AbortController | null>(null)
const [currentToolName, setCurrentToolName] = useState<string | undefined>(undefined)
// 使用 ref 保存从 location.state 传递的 toolId,避免被异步操作覆盖
const toolIdFromStateRef = useRef<string | null | undefined>(undefined)
// 历史记录点击时将 toolId 通过路由 state 传入,优先使用该值快速同步高亮
useEffect(() => {
// 保存从 location.state 传递的 toolId 到 ref
toolIdFromStateRef.current = initialToolId
if (typeof initialToolId === 'undefined')
return
if (initialToolId) {
dispatch(setCurrentToolId(initialToolId))
// 统一转换为字符串,确保类型一致(真实API可能返回数字,需要转换为字符串)
const normalizedToolId = String(initialToolId)
// eslint-disable-next-line no-console
console.log('[Chat] 从路由state设置toolId:', {
originalToolId: initialToolId,
originalType: typeof initialToolId,
normalizedToolId,
normalizedType: typeof normalizedToolId,
})
dispatch(setCurrentToolId(normalizedToolId))
}
else {
dispatch(clearCurrentToolId())
......@@ -250,19 +278,56 @@ export const Chat: React.FC = () => {
// 如果 qaRecords 中没有 toolId,尝试从 conversations 中查找当前会话的 toolId(会话级别)
const conversationToolId = latestToolId || (conversations.find(conv => conv.conversationId === conversationId)?.toolId?.trim?.())
// 优先使用从 location.state 传递的 toolId(如果存在),这是从历史记录点击传递过来的
const toolIdFromState = toolIdFromStateRef.current !== undefined
? (toolIdFromStateRef.current ? String(toolIdFromStateRef.current) : null)
: undefined
// eslint-disable-next-line no-console
console.log('[Chat] 从历史记录获取 toolId:', {
conversationId,
toolIdFromState,
latestToolIdFromQaRecords: latestToolId,
conversationToolId,
hasQaRecords,
currentToolIdInRedux: currentToolId,
})
// 确定最终使用的 toolId:优先使用 qaRecords 中的,其次使用 conversation 中的
const finalToolId = latestToolId || conversationToolId || undefined
// 确定最终使用的 toolId:优先使用从 location.state 传递的,其次使用 qaRecords 中的,最后使用 conversation 中的
// 如果从 location.state 传递了 toolId,直接使用它(最高优先级)
if (toolIdFromState !== undefined) {
if (toolIdFromState) {
// 只有当 Redux 中的 toolId 与最终确定的 toolId 不一致时,才更新
if (currentToolId !== toolIdFromState) {
// eslint-disable-next-line no-console
console.log('[Chat] 使用从路由state传递的 toolId:', {
from: currentToolId,
to: toolIdFromState,
source: 'location.state',
})
dispatch(setCurrentToolId(toolIdFromState))
}
else {
// eslint-disable-next-line no-console
console.log('[Chat] toolId 已一致,无需更新:', toolIdFromState)
}
}
else {
// 如果从 location.state 传递的是 null,清除 toolId
if (currentToolId) {
// eslint-disable-next-line no-console
console.log('[Chat] 清除 toolId (从路由state传递null)')
dispatch(clearCurrentToolId())
}
}
// 清除 ref,避免下次路由变化时误用
toolIdFromStateRef.current = undefined
}
else {
// 如果没有从 location.state 传递 toolId(可能是嵌套路由传递失败),使用原来的逻辑
// 优先使用 qaRecords 中的 toolId,其次使用 conversation 中的 toolId
const finalToolId = latestToolId || conversationToolId || undefined
if (hasQaRecords || conversationToolId) {
if (finalToolId) {
// 只有当 Redux 中的 toolId 与最终确定的 toolId 不一致时,才更新
if (currentToolId !== finalToolId) {
......@@ -280,20 +345,19 @@ export const Chat: React.FC = () => {
}
}
else {
// 如果 qaRecords 和 conversation 中都没有 toolId,清除 Redux 中的 toolId
if (currentToolId) {
// 如果 qaRecords 和 conversation 中都没有 toolId
// 如果有历史记录但没有 toolId,说明是通用模式,应该清除
if (hasQaRecords && currentToolId) {
// eslint-disable-next-line no-console
console.log('[Chat] 清除 toolId (qaRecords 和 conversation 中都没有)')
console.log('[Chat] 清除 toolId (qaRecords 中有记录但没有 toolId,通用模式)')
dispatch(clearCurrentToolId())
}
}
}
else {
// 如果没有 qaRecords 且没有 conversation,清除 toolId
if (currentToolId) {
// eslint-disable-next-line no-console
console.log('[Chat] 清除 toolId (没有历史记录)')
dispatch(clearCurrentToolId())
// 如果没有历史记录,可能是新会话,但如果 Redux 中已经有 toolId(从 HistoryBarList 设置的),暂时保留
// 因为可能是刚点击历史记录但 API 还没返回,或者 location.state 传递失败但 Redux 中已有正确的值
else if (!hasQaRecords && currentToolId) {
// eslint-disable-next-line no-console
console.log('[Chat] 没有历史记录,保留 Redux 中的 toolId (可能是 location.state 传递失败):', currentToolId)
}
}
}
}
......@@ -347,7 +411,9 @@ export const Chat: React.FC = () => {
const getToolNameFromToolId = async () => {
if (currentToolId) {
try {
// 使用统一的方法获取 userRoles(先同步路由到 localStorage,然后读取)
// 使用mock数据(已注释)
// const res = await mockFetchToolList()
// 真实API调用
const userRoles = getUserRolesForApi()
const res = await fetchToolList({ userRoles })
if (res?.data) {
......@@ -414,12 +480,15 @@ export const Chat: React.FC = () => {
className={`${styles.scrollable} scrollbar-hide scroll-smooth`}
>
<div className={styles.inter}>
{allItems.map((record, index) => (
{allItems.map((record, index) => {
const recordId = record.answerList?.[0]?.recordId || record.groupId
const uniqueKey = recordId
? `${record.role}-${recordId}`
: `${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}`
return (
<div
className="w-full chatItem mx-auto"
key={`${record.role}-${index}-${
record.question || record.answerList?.[0]?.answer || ''
}`}
key={uniqueKey}
>
{record.role === 'system' && <ChatWelcome toolName={currentToolName} />}
{record.role === 'user' && <ChatItemUser record={record} />}
......@@ -433,7 +502,8 @@ export const Chat: React.FC = () => {
/>
)}
</div>
))}
)
})}
</div>
</motion.div>
)}
......
......@@ -87,7 +87,16 @@ export const Home: React.FC = () => {
}))
setIsDataLoaded(false) // 重置加载状态
try {
const res = await fetchEfficiencyQuestionList({ toolId })
// 获取 toolId:优先使用传入参数,其次从 sessionStorage,再次从路由参数,都没有则使用空字符串
let finalToolId = toolId || ''
if (!finalToolId) {
finalToolId = sessionStorage.getItem('currentToolId') || ''
}
if (!finalToolId) {
const searchParams = new URLSearchParams(location.search)
finalToolId = searchParams.get('toolId') || ''
}
const res = await fetchEfficiencyQuestionList({ toolId: finalToolId })
if (res && res.data && res.data.questions) {
setOtherQuestions((prev: any) => ({
...prev,
......@@ -105,7 +114,7 @@ export const Home: React.FC = () => {
// else if (isToolBtn) {
// setIsDataLoaded(true) // 恢复原始数据时标记为已加载
// }
}, [originalOtherQuestions])
}, [originalOtherQuestions, location.search])
// 监听工具按钮点击事件
useEffect(() => {
......
......@@ -3,6 +3,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { processConversationData } from './conversationSlice.helper'
import type { Conversation, ConversationState } from '@/types/conversation'
import { fetchCreateConversation, fetchDeleteUserConversation, fetchQueryUserConversationPage } from '@/api/conversation'
// import { mockFetchConversationPage } from '@/api/mock/conversation'
const initialState: ConversationState = {
conversations: [],
......@@ -18,6 +19,9 @@ export const fetchConversations = createAsyncThunk(
'conversation/fetchConversations',
async (_, { rejectWithValue }) => {
try {
// 使用mock数据(已注释)
// const response = await mockFetchConversationPage()
// 真实API调用
const response = await fetchQueryUserConversationPage({
keyword: '',
pageNum: 0,
......
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