Commit d586835c by Liu

feat:新增页面内会话部分逻辑

parent 9bf64ec0
import type React from 'react' import type React from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useClickAway, useSessionStorageState } from 'ahooks' import { useClickAway, useSessionStorageState } from 'ahooks'
import styles from './Navbar.module.less' import styles from './Navbar.module.less'
import { NavBarItem } from './components/NavBarItem' import { NavBarItem } from './components/NavBarItem'
import { clearNavigationFlag, createConversation } from '@/store/conversationSlice' import { clearNavigationFlag, createConversation } from '@/store/conversationSlice'
import { createTacticsConversation } from '@/store/tacticsSlice'
import type { WithAuthProps } from '@/auth/withAuth' import type { WithAuthProps } from '@/auth/withAuth'
import { withAuth } from '@/auth/withAuth' import { withAuth } from '@/auth/withAuth'
import { NAV_BAR_ITEMS } from '@/config/nav' import { NAV_BAR_ITEMS } from '@/config/nav'
...@@ -21,8 +22,10 @@ interface NavbarProps { ...@@ -21,8 +22,10 @@ interface NavbarProps {
const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, checkAuth, onSetHistoryVisible }) => { const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, checkAuth, onSetHistoryVisible }) => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
const { currentConversationId, shouldNavigateToNewConversation, currentToolId } = useAppSelector(state => state.conversation) const { currentConversationId, shouldNavigateToNewConversation, currentToolId } = useAppSelector(state => state.conversation)
const { currentConversationId: _tacticsConversationId, shouldNavigateToNewConversation: _tacticsShouldNavigate } = useAppSelector(state => state.tactics)
const handleCreateConversation = () => { const handleCreateConversation = () => {
const sessionToolId = sessionStorage.getItem('currentToolId') || undefined const sessionToolId = sessionStorage.getItem('currentToolId') || undefined
...@@ -35,6 +38,14 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c ...@@ -35,6 +38,14 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
})) }))
} }
const handleCreateTacticsConversation = () => {
dispatch(createTacticsConversation({
conversationData: {},
shouldNavigate: true,
shouldSendQuestion: '',
}))
}
const [isH5NavVisible, setIsH5NavVisible] = useState(isMobile()) const [isH5NavVisible, setIsH5NavVisible] = useState(isMobile())
const handleClick = (type: string | undefined) => { const handleClick = (type: string | undefined) => {
...@@ -54,7 +65,11 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c ...@@ -54,7 +65,11 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
} }
if (type === 'add') { if (type === 'add') {
if (location.pathname.includes('/chat')) { // 判断是否为 tactics 聊天页面
if (location.pathname.startsWith('/tactics/chat')) {
handleCreateTacticsConversation()
}
else if (location.pathname.includes('/chat')) {
handleCreateConversation() handleCreateConversation()
} }
else { else {
...@@ -110,7 +125,7 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c ...@@ -110,7 +125,7 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
const url = currentToolId const url = currentToolId
? `/chat/${currentConversationId}?toolId=${currentToolId}` ? `/chat/${currentConversationId}?toolId=${currentToolId}`
: `/chat/${currentConversationId}` : `/chat/${currentConversationId}`
// 通过 location.state 传递 toolId,避免在 Chat 页面被误判为“外链残留参数”而强制清空 // 通过 location.state 传递 toolId,避免在 Chat 页面被误判为"外链残留参数"而强制清空
navigate(url, { navigate(url, {
state: { state: {
toolId: currentToolId || null, toolId: currentToolId || null,
......
// 问答功能独立聊天页 // 问答功能独立聊天页
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useLocalStorageState, useScroll } from 'ahooks'
import { Button } from '@heroui/react' import { Button } from '@heroui/react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { useScroll } from 'ahooks'
import styles from '../Chat/Chat.module.less' import styles from '../Chat/Chat.module.less'
import { processApiResponse } from '../Chat/helper' import { processApiResponse } from '../Chat/helper'
import { ChatItemUser } from '../Chat/components/ChatItem/ChatItemUser' import { ChatItemUser } from '../Chat/components/ChatItem/ChatItemUser'
...@@ -13,7 +13,8 @@ import { ChatEditor } from '@/components/ChatEditor' ...@@ -13,7 +13,8 @@ import { ChatEditor } from '@/components/ChatEditor'
import type { ChatRecord } from '@/types/chat' import type { ChatRecord } from '@/types/chat'
import { fetchTacticsQaRecordPage } from '@/api/tactics' import { fetchTacticsQaRecordPage } from '@/api/tactics'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat' import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
import { clearTacticsNavigationFlag, clearTacticsShouldSendQuestion, createTacticsConversation } from '@/store/tacticsSlice' import { fetchLoginByToken, fetchLoginByUid } from '@/api/common'
import { clearTacticsNavigationFlag, clearTacticsShouldSendQuestion, createTacticsConversation, fetchTacticsConversations } from '@/store/tacticsSlice'
import type { RootState } from '@/store' import type { RootState } from '@/store'
import { useAppDispatch, useAppSelector } from '@/store/hook' import { useAppDispatch, useAppSelector } from '@/store/hook'
import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react' import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
...@@ -24,6 +25,7 @@ export const TacticsChat: React.FC = () => { ...@@ -24,6 +25,7 @@ export const TacticsChat: React.FC = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [allItems, setAllItems] = useState<ChatRecord[]>([]) const [allItems, setAllItems] = useState<ChatRecord[]>([])
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
...@@ -32,6 +34,12 @@ export const TacticsChat: React.FC = () => { ...@@ -32,6 +34,12 @@ export const TacticsChat: React.FC = () => {
shouldNavigateToNewConversation, shouldNavigateToNewConversation,
currentConversationId, currentConversationId,
} = useAppSelector((state: RootState) => state.tactics) } = useAppSelector((state: RootState) => state.tactics)
const hasFetched = useRef(false)
const hasCreatedRef = useRef(false)
// 使用 useLocalStorageState 管理 token,与原有逻辑保持一致
const [token, setToken] = useLocalStorageState<string | undefined>('__TOKEN__', {
defaultValue: '',
})
// 优先从 location.state 获取,其次从 Redux state 获取 // 优先从 location.state 获取,其次从 Redux state 获取
const shouldSendQuestion = (location.state as { shouldSendQuestion?: string } | null)?.shouldSendQuestion || shouldSendQuestionFromState const shouldSendQuestion = (location.state as { shouldSendQuestion?: string } | null)?.shouldSendQuestion || shouldSendQuestionFromState
const scrollableRef = useRef<HTMLDivElement | any>(null) const scrollableRef = useRef<HTMLDivElement | any>(null)
...@@ -39,7 +47,79 @@ export const TacticsChat: React.FC = () => { ...@@ -39,7 +47,79 @@ export const TacticsChat: React.FC = () => {
const currentIdRef = useRef<string | undefined>(id) const currentIdRef = useRef<string | undefined>(id)
const lastSentQuestionRef = useRef<string>('') const lastSentQuestionRef = useRef<string>('')
const abortControllerRef = useRef<AbortController | null>(null) const abortControllerRef = useRef<AbortController | null>(null)
const hasCreatedRef = useRef(false)
// 创建新会话(仅在 tactics 聊天页面且没有 id 时调用)
const initTacticsConversation = useCallback(() => {
// 只有在 tactics 聊天页面且没有会话 id 时才创建新对话
if (!id && location.pathname.startsWith('/tactics/chat')) {
if (hasCreatedRef.current) {
return
}
hasCreatedRef.current = true
dispatch(
createTacticsConversation({
conversationData: {},
shouldNavigate: true,
shouldSendQuestion: '',
}),
)
}
}, [id, location.pathname, dispatch])
// 登录逻辑(复用原有逻辑,与 TacticsHome.tsx 保持一致)
const login = useCallback(async () => {
if (hasFetched.current) {
return
}
hasFetched.current = true
const url = new URL(window.location.href)
const searchParams = new URLSearchParams(url.search)
const _loginCode = searchParams.get('loginCode')
let res = {} as any
if (viteOutputObj === 'inner') {
if (_loginCode) {
res = await fetchLoginByToken(_loginCode)
if (res.data) {
setToken(res.data.token)
window.dispatchEvent(
new StorageEvent('storage', {
key: '__TOKEN__',
oldValue: token,
newValue: res.data.token,
url: window.location.href,
storageArea: localStorage,
}),
)
// 登录成功后,如果是 tactics 聊天页面且没有 id,则创建会话
initTacticsConversation()
dispatch(fetchTacticsConversations())
}
}
else {
// 如果没有 loginCode,但已有 token,直接尝试创建会话
initTacticsConversation()
dispatch(fetchTacticsConversations())
}
}
else {
res = await fetchLoginByUid('123123')
if (res.data) {
setToken(res.data.token)
window.dispatchEvent(
new StorageEvent('storage', {
key: '__TOKEN__',
oldValue: token,
newValue: res.data.token,
url: window.location.href,
storageArea: localStorage,
}),
)
// 登录成功后,如果是 tactics 聊天页面且没有 id,则创建会话
initTacticsConversation()
dispatch(fetchTacticsConversations())
}
}
}, [setToken, dispatch, token, initTacticsConversation, viteOutputObj])
/** 处理正常stream的数据 */ /** 处理正常stream的数据 */
const handleStreamMesageData = (msg: any, question: string) => { const handleStreamMesageData = (msg: any, question: string) => {
...@@ -229,22 +309,17 @@ export const TacticsChat: React.FC = () => { ...@@ -229,22 +309,17 @@ export const TacticsChat: React.FC = () => {
getUserQaRecordPage(id) getUserQaRecordPage(id)
} }
else { else {
// 如果没有 id,进入页面时创建新会话 // 如果没有 id,显示欢迎语,等待登录成功后创建新会话
if (!hasCreatedRef.current) {
hasCreatedRef.current = true
dispatch(
createTacticsConversation({
conversationData: {},
shouldNavigate: true,
shouldSendQuestion: '',
}),
)
}
setAllItems([{ role: 'system' } as ChatRecord]) setAllItems([{ role: 'system' } as ChatRecord])
setIsLoading(false) setIsLoading(false)
} }
}, [id, getUserQaRecordPage, dispatch]) }, [id, getUserQaRecordPage, dispatch])
// 初始化时调用登录(登录成功后会自动创建会话)
useEffect(() => {
login()
}, [login])
// 创建新会话成功后跳转到新会话页面 // 创建新会话成功后跳转到新会话页面
useEffect(() => { useEffect(() => {
if (shouldNavigateToNewConversation && currentConversationId) { if (shouldNavigateToNewConversation && currentConversationId) {
......
...@@ -12,6 +12,7 @@ import { fetchLoginByToken, fetchLoginByUid } from '@/api/common' ...@@ -12,6 +12,7 @@ import { fetchLoginByToken, fetchLoginByUid } from '@/api/common'
import { getUserRolesFromRoute } from '@/lib/utils' import { getUserRolesFromRoute } from '@/lib/utils'
import { ChatEditor } from '@/components/ChatEditor' import { ChatEditor } from '@/components/ChatEditor'
import { RECOMMEND_QUESTIONS_OTHER } from '@/config/recommendQuestion' import { RECOMMEND_QUESTIONS_OTHER } from '@/config/recommendQuestion'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
export const TacticsHome: React.FC = () => { export const TacticsHome: React.FC = () => {
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open' const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
...@@ -24,6 +25,7 @@ export const TacticsHome: React.FC = () => { ...@@ -24,6 +25,7 @@ export const TacticsHome: React.FC = () => {
const [token, setToken] = useLocalStorageState<string | undefined>('__TOKEN__', { const [token, setToken] = useLocalStorageState<string | undefined>('__TOKEN__', {
defaultValue: '', defaultValue: '',
}) })
const abortControllerRef = useRef<AbortController | null>(null)
const initTacticsConversation = () => { const initTacticsConversation = () => {
const fromCollect = location.state?.fromCollect const fromCollect = location.state?.fromCollect
...@@ -44,7 +46,53 @@ export const TacticsHome: React.FC = () => { ...@@ -44,7 +46,53 @@ export const TacticsHome: React.FC = () => {
} }
// 处理创建对话并跳转(用于输入框提交) // 处理创建对话并跳转(用于输入框提交)
const handleCreateConversation = useCallback((question: string) => { const handleCreateConversation = useCallback(async (question: string) => {
// 如果已有会话,直接调用 submit 接口提交问题,然后跳转到聊天页面
if (currentConversationId) {
// 停止之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
// 检查token
await fetchCheckTokenApi()
// 创建新的 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
// 直接调用 submit 接口(消息流会在聊天页面处理,这里只负责发起请求)
fetchStreamResponse(
fetchUrl,
{
question,
conversationId: currentConversationId,
stream: true,
},
() => {
// 在首页不需要处理消息,跳转到聊天页面后会自动刷新加载最新消息
},
abortControllerRef.current.signal,
)
// 跳转到聊天页面查看结果
navigate(`/tactics/chat/${currentConversationId}`)
return
}
// 如果没有会话,才创建新会话
dispatch( dispatch(
createTacticsConversation({ createTacticsConversation({
conversationData: {}, conversationData: {},
...@@ -52,7 +100,7 @@ export const TacticsHome: React.FC = () => { ...@@ -52,7 +100,7 @@ export const TacticsHome: React.FC = () => {
shouldSendQuestion: question, shouldSendQuestion: question,
}), }),
) )
}, [dispatch]) }, [dispatch, currentConversationId, navigate])
// 监听导航到新对话 // 监听导航到新对话
useEffect(() => { useEffect(() => {
......
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