Commit d586835c by Liu

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

parent 9bf64ec0
import type React from 'react'
import { motion } from 'framer-motion'
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 styles from './Navbar.module.less'
import { NavBarItem } from './components/NavBarItem'
import { clearNavigationFlag, createConversation } from '@/store/conversationSlice'
import { createTacticsConversation } from '@/store/tacticsSlice'
import type { WithAuthProps } from '@/auth/withAuth'
import { withAuth } from '@/auth/withAuth'
import { NAV_BAR_ITEMS } from '@/config/nav'
......@@ -21,8 +22,10 @@ interface NavbarProps {
const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, checkAuth, onSetHistoryVisible }) => {
const dispatch = useAppDispatch()
const navigate = useNavigate()
const location = useLocation()
const { currentConversationId, shouldNavigateToNewConversation, currentToolId } = useAppSelector(state => state.conversation)
const { currentConversationId: _tacticsConversationId, shouldNavigateToNewConversation: _tacticsShouldNavigate } = useAppSelector(state => state.tactics)
const handleCreateConversation = () => {
const sessionToolId = sessionStorage.getItem('currentToolId') || undefined
......@@ -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 handleClick = (type: string | undefined) => {
......@@ -54,7 +65,11 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
}
if (type === 'add') {
if (location.pathname.includes('/chat')) {
// 判断是否为 tactics 聊天页面
if (location.pathname.startsWith('/tactics/chat')) {
handleCreateTacticsConversation()
}
else if (location.pathname.includes('/chat')) {
handleCreateConversation()
}
else {
......@@ -110,7 +125,7 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
const url = currentToolId
? `/chat/${currentConversationId}?toolId=${currentToolId}`
: `/chat/${currentConversationId}`
// 通过 location.state 传递 toolId,避免在 Chat 页面被误判为“外链残留参数”而强制清空
// 通过 location.state 传递 toolId,避免在 Chat 页面被误判为"外链残留参数"而强制清空
navigate(url, {
state: {
toolId: currentToolId || null,
......
// 问答功能独立聊天页
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useLocalStorageState, useScroll } from 'ahooks'
import { Button } from '@heroui/react'
import { motion } from 'framer-motion'
import { useScroll } from 'ahooks'
import styles from '../Chat/Chat.module.less'
import { processApiResponse } from '../Chat/helper'
import { ChatItemUser } from '../Chat/components/ChatItem/ChatItemUser'
......@@ -13,7 +13,8 @@ import { ChatEditor } from '@/components/ChatEditor'
import type { ChatRecord } from '@/types/chat'
import { fetchTacticsQaRecordPage } from '@/api/tactics'
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 { useAppDispatch, useAppSelector } from '@/store/hook'
import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
......@@ -24,6 +25,7 @@ export const TacticsChat: React.FC = () => {
const { id } = useParams<{ id: string }>()
const location = useLocation()
const navigate = useNavigate()
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
const [isLoading, setIsLoading] = useState(false)
const [allItems, setAllItems] = useState<ChatRecord[]>([])
const dispatch = useAppDispatch()
......@@ -32,6 +34,12 @@ export const TacticsChat: React.FC = () => {
shouldNavigateToNewConversation,
currentConversationId,
} = 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 获取
const shouldSendQuestion = (location.state as { shouldSendQuestion?: string } | null)?.shouldSendQuestion || shouldSendQuestionFromState
const scrollableRef = useRef<HTMLDivElement | any>(null)
......@@ -39,7 +47,79 @@ export const TacticsChat: React.FC = () => {
const currentIdRef = useRef<string | undefined>(id)
const lastSentQuestionRef = useRef<string>('')
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的数据 */
const handleStreamMesageData = (msg: any, question: string) => {
......@@ -229,22 +309,17 @@ export const TacticsChat: React.FC = () => {
getUserQaRecordPage(id)
}
else {
// 如果没有 id,进入页面时创建新会话
if (!hasCreatedRef.current) {
hasCreatedRef.current = true
dispatch(
createTacticsConversation({
conversationData: {},
shouldNavigate: true,
shouldSendQuestion: '',
}),
)
}
// 如果没有 id,显示欢迎语,等待登录成功后创建新会话
setAllItems([{ role: 'system' } as ChatRecord])
setIsLoading(false)
}
}, [id, getUserQaRecordPage, dispatch])
// 初始化时调用登录(登录成功后会自动创建会话)
useEffect(() => {
login()
}, [login])
// 创建新会话成功后跳转到新会话页面
useEffect(() => {
if (shouldNavigateToNewConversation && currentConversationId) {
......
......@@ -12,6 +12,7 @@ import { fetchLoginByToken, fetchLoginByUid } from '@/api/common'
import { getUserRolesFromRoute } from '@/lib/utils'
import { ChatEditor } from '@/components/ChatEditor'
import { RECOMMEND_QUESTIONS_OTHER } from '@/config/recommendQuestion'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
export const TacticsHome: React.FC = () => {
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
......@@ -24,6 +25,7 @@ export const TacticsHome: React.FC = () => {
const [token, setToken] = useLocalStorageState<string | undefined>('__TOKEN__', {
defaultValue: '',
})
const abortControllerRef = useRef<AbortController | null>(null)
const initTacticsConversation = () => {
const fromCollect = location.state?.fromCollect
......@@ -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(
createTacticsConversation({
conversationData: {},
......@@ -52,7 +100,7 @@ export const TacticsHome: React.FC = () => {
shouldSendQuestion: question,
}),
)
}, [dispatch])
}, [dispatch, currentConversationId, navigate])
// 监听导航到新对话
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