Commit b4dc7dee by Liu

feat:新建会话逻辑&&重新分析&&清除记录

parent 887c7712
import React, { useRef, useState } from 'react'
import { Button, Tooltip } from '@heroui/react'
import SendIcon from '@/assets/svg/send.svg?react'
import { type WithAuthProps, withAuth } from '@/auth/withAuth'
import { useAppSelector } from '@/store/hook'
import type { RootState } from '@/store'
interface TacticsChatEditorProps {
onSubmit?: (value: string) => void
onChange?: (value: string) => void
placeholders?: string[]
initialValue?: string
}
const defaultPlaceholders: string[] = []
const TacticsChatEditorBase: React.FC<TacticsChatEditorProps & WithAuthProps> = ({
onSubmit,
onChange,
placeholders = defaultPlaceholders,
initialValue = '',
checkAuth,
}) => {
const [content, setContent] = useState(initialValue)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const isAsking = useAppSelector((state: RootState) => state.chat.isAsking)
const [placeholderIndex] = useState(0)
const currentPlaceholder = placeholders[placeholderIndex] || '输入您想问的问题吧'
const adjustHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
}
}
const handleChange = (value: string) => {
setContent(value)
onChange?.(value)
adjustHeight()
}
const handleSubmit = () => {
// eslint-disable-next-line no-console
console.log('[TacticsChatEditor] submit click', {
content,
isAsking,
hasOnSubmit: Boolean(onSubmit),
})
if (isAsking) {
// eslint-disable-next-line no-console
console.log('[TacticsChatEditor] blocked: isAsking true')
return
}
const authed = checkAuth()
// eslint-disable-next-line no-console
console.log('[TacticsChatEditor] auth checked', { authed })
if (!authed)
return
const trimmed = content.trim()
if (!trimmed) {
// eslint-disable-next-line no-console
console.log('[TacticsChatEditor] blocked: empty content')
return
}
// eslint-disable-next-line no-console
console.log('[TacticsChatEditor] fire onSubmit', { trimmed, hasOnSubmit: Boolean(onSubmit) })
try {
onSubmit?.(trimmed)
// eslint-disable-next-line no-console
console.log('[TacticsChatEditor] after onSubmit')
}
catch (error) {
console.error('[TacticsChatEditor] onSubmit error', error)
}
setContent('')
if (textareaRef.current) {
textareaRef.current.value = ''
adjustHeight()
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}
return (
<div className="flex items-end w-full h-auto relative mx-auto rounded-[24px] overflow-hidden transition duration-200 py-[8px] pl-[12px] pr-[12px] bg-white border border-[#0a0a0a78] shadow-[0_8px_12px_0_rgba(235,235,235,0.30)]">
<textarea
ref={textareaRef}
defaultValue={initialValue}
className="w-full min-h-[40px] max-h-[120px] p-2 rounded overflow-y-auto outline-none resize-none"
placeholder={currentPlaceholder}
onChange={e => handleChange(e.target.value)}
onKeyDown={handleKeyDown}
rows={1}
/>
<Tooltip
isOpen={!content && !isAsking}
color="foreground"
content="请输入您的问题📖"
placement="top-end"
>
<Button
className="ask-send"
onPress={handleSubmit}
radius="full"
isDisabled={!content || isAsking}
isIconOnly
color="primary"
>
<SendIcon />
</Button>
</Tooltip>
</div>
)
}
export const TacticsChatEditor = withAuth(TacticsChatEditorBase)
export default TacticsChatEditor
......@@ -24,8 +24,8 @@
overflow: hidden;
}
.scrollable {
flex-direction: column-reverse;
align-items: center;
flex-direction: column;
align-items: stretch;
display: flex;
overflow-x: hidden;
overflow-y: scroll;
......
// 问答功能独立聊天页
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useLocalStorageState, useScroll } from 'ahooks'
import { Button } from '@heroui/react'
import { motion } from 'framer-motion'
......@@ -9,12 +9,14 @@ import { processApiResponse } from '../Chat/helper'
import { ChatItemUser } from '../Chat/components/ChatItem/ChatItemUser'
import { ChatAnswerBox } from '../Chat/components/ChatItem/ChatAnswerBox'
import { TacticsWelcome } from './components/TacticsWelcome'
import { ChatEditor } from '@/components/ChatEditor'
import DeleteIcon from '@/assets/svg/delete.svg?react'
import RefreshIcon from '@/assets/svg/refresh.svg?react'
import { TacticsChatEditor } from '@/components/TacticsChatEditor'
import type { ChatRecord } from '@/types/chat'
import { fetchTacticsQaRecordPage } from '@/api/tactics'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
import { fetchLoginByToken, fetchLoginByUid } from '@/api/common'
import { clearTacticsNavigationFlag, clearTacticsShouldSendQuestion, createTacticsConversation, fetchTacticsConversations } from '@/store/tacticsSlice'
import { clearTacticsNavigationFlag, clearTacticsShouldSendQuestion, createTacticsConversation, deleteTacticsConversations, fetchTacticsConversations } from '@/store/tacticsSlice'
import type { RootState } from '@/store'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
......@@ -28,12 +30,15 @@ export const TacticsChat: React.FC = () => {
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
const [isLoading, setIsLoading] = useState(false)
const [allItems, setAllItems] = useState<ChatRecord[]>([])
const [historyDividerIndex, setHistoryDividerIndex] = useState<number | null>(null)
const [historyDividerTime, setHistoryDividerTime] = useState<string | null>(null)
const dispatch = useAppDispatch()
const {
shouldSendQuestion: shouldSendQuestionFromState,
shouldNavigateToNewConversation,
currentConversationId,
} = useAppSelector((state: RootState) => state.tactics)
const [searchParams] = useSearchParams()
const hasFetched = useRef(false)
const hasCreatedRef = useRef(false)
// 使用 useLocalStorageState 管理 token,与原有逻辑保持一致
......@@ -47,6 +52,47 @@ export const TacticsChat: React.FC = () => {
const currentIdRef = useRef<string | undefined>(id)
const lastSentQuestionRef = useRef<string>('')
const abortControllerRef = useRef<AbortController | null>(null)
// 读取 from=tactics 且 version=2 的额外参数
const tacticsMetaFromState = (location.state as { tacticsMeta?: any } | null)?.tacticsMeta
const tacticsMetaFromQuery = useMemo(() => {
const from = searchParams.get('from')
const version = searchParams.get('version')
if (from !== 'tactics' || version !== '2') {
return undefined
}
return {
version,
taskId: searchParams.get('taskId') || undefined,
pinBeginTime: searchParams.get('pinBeginTime') || undefined,
pinEndTime: searchParams.get('pinEndTime') || undefined,
partOrAll: searchParams.get('partOrAll') || 'all',
channel: searchParams.get('channel') || 'AIphone',
channelName: searchParams.get('channelName') || undefined,
}
}, [searchParams])
const tacticsMetaFromStorage = useMemo(() => {
const raw = sessionStorage.getItem('tacticsMeta')
if (!raw) {
return undefined
}
try {
const parsed = JSON.parse(raw)
if (parsed?.version === '2') {
return parsed
}
}
catch {
// ignore
}
return undefined
}, [])
const tacticsMeta = useMemo(() => tacticsMetaFromState || tacticsMetaFromQuery || tacticsMetaFromStorage, [tacticsMetaFromState, tacticsMetaFromQuery, tacticsMetaFromStorage])
const formatCurrentTime = () => {
const date = new Date()
const pad = (value: number) => value.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
// 创建新会话(仅在 tactics 聊天页面且没有 id 时调用)
const initTacticsConversation = useCallback(() => {
......@@ -124,11 +170,14 @@ export const TacticsChat: React.FC = () => {
/** 处理正常stream的数据 */
const handleStreamMesageData = (msg: any, question: string) => {
setAllItems((prevItems) => {
const newItems = [...prevItems]
const newItems = [...prevItems] // 创建数组的浅拷贝
const lastIndex = newItems.length - 1
if (lastIndex >= 0) {
// 创建最后一项的新对象,合并现有数据和新的 answer
const originalAnswer = (newItems[lastIndex].answerList?.[0]?.answer || '') + msg.content.data.answer
// 移除所有括号及其内容
let filteredAnswer = originalAnswer.replace(/\([^)]*\)/g, '').trim()
// 去除 [参考文档《任意内容》 《任意内容》...] 格式的内容
filteredAnswer = filteredAnswer.replace(/\[参考文档(?:[^]*》\s*)+\]/g, '').trim()
newItems[lastIndex] = {
......@@ -172,7 +221,24 @@ export const TacticsChat: React.FC = () => {
}
/** 提交问题 */
const handleSubmitQuestion = async (question: string) => {
const handleSubmitQuestion = async (
question: string | undefined,
productCode?: string,
toolId?: string,
extra?: { busiType?: string, recordType?: string, includeQuestion?: boolean },
) => {
// eslint-disable-next-line no-console
console.log('[TacticsChat] handleSubmitQuestion:start', {
question,
productCode,
toolIdFromProp: toolId,
sessionToolId: sessionStorage.getItem('currentToolId'),
currentId: currentIdRef.current,
extra,
})
// 优先读取缓存中的 toolId,再回退到传参
const sessionToolId = sessionStorage.getItem('currentToolId') ?? undefined
const resolvedToolId = toolId ?? sessionToolId ?? undefined
// 停止之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort()
......@@ -183,14 +249,21 @@ export const TacticsChat: React.FC = () => {
// 检查token
await fetchCheckTokenApi()
// eslint-disable-next-line no-console
console.log('[TacticsChat] handleSubmitQuestion:afterCheckToken', { currentId: currentIdRef.current })
// 一次性添加用户问题和空的AI回答
// 一次性添加用户问题和空的AI回答;如果不需要问题(自动触发场景),只添加 AI 占位
const shouldIncludeQuestion = extra?.includeQuestion !== false && !!question
setAllItems(prevItems => [
...prevItems,
...(shouldIncludeQuestion
? [
{
role: 'user',
question,
} as ChatRecord,
]
: []),
{
role: 'ai',
answerList: [{ answer: '' }],
......@@ -214,14 +287,33 @@ export const TacticsChat: React.FC = () => {
fetchUrl = proxy + fetchUrl
fetchStreamResponse(
fetchUrl,
{
question,
const shouldSendQuestion = extra?.includeQuestion !== false
const requestBody: Record<string, any> = {
conversationId: currentIdRef.current,
stream: true,
},
productCode,
toolId: resolvedToolId,
...(tacticsMeta || {}),
...(extra?.busiType ? { busiType: extra.busiType } : {}),
...(extra?.recordType ? { recordType: extra.recordType } : {}),
}
if (shouldSendQuestion) {
requestBody.question = question ?? ''
}
// eslint-disable-next-line no-console
console.log('[TacticsChat] handleSubmitQuestion:requesting', {
fetchUrl,
body: requestBody,
})
fetchStreamResponse(
fetchUrl,
requestBody,
(msg) => {
// eslint-disable-next-line no-console
console.log('[TacticsChat] handleSubmitQuestion:onMessage', msg)
// 检查是否已被取消
if (abortControllerRef.current?.signal.aborted) {
return
......@@ -229,6 +321,7 @@ export const TacticsChat: React.FC = () => {
// 处理错误
if (msg?.type === 'ERROR') {
// 如果是 AbortError,不显示错误
if (msg.content?.name === 'AbortError') {
return
}
......@@ -237,17 +330,17 @@ export const TacticsChat: React.FC = () => {
// 正常的stream数据
if (msg?.type === 'DATA' && msg?.content?.code === '00000000') {
handleStreamMesageData(msg, question)
handleStreamMesageData(msg, question || '')
}
if (msg?.type === 'DATA' && msg?.content?.code === '01010005') {
handleChatMaxCount(msg, question)
handleChatMaxCount(msg, question || '')
return
}
if (msg.type === 'END') {
dispatch(setIsAsking(false))
if (isNew) {
setTimeout(() => {
// 可以在这里刷新会话列表
dispatch(fetchTacticsConversations())
}, 2000)
}
}
......@@ -264,6 +357,9 @@ export const TacticsChat: React.FC = () => {
const qaRecords = res.data || []
// 始终添加 system 角色作为欢迎语
const messages = [{ role: 'system' } as ChatRecord, ...processApiResponse(qaRecords)]
const hasHistory = qaRecords.length > 0
setHistoryDividerIndex(hasHistory ? messages.length : null)
setHistoryDividerTime(hasHistory ? formatCurrentTime() : null)
// 处理历史记录中的参考文档标记
const processedMessages = messages.map((item) => {
if (item.role === 'ai' && item.answerList?.[0]?.answer) {
......@@ -285,6 +381,8 @@ export const TacticsChat: React.FC = () => {
catch {
// 如果获取失败,至少显示欢迎语
setAllItems([{ role: 'system' } as ChatRecord])
setHistoryDividerIndex(null)
setHistoryDividerTime(null)
}
finally {
setIsLoading(false)
......@@ -296,6 +394,34 @@ export const TacticsChat: React.FC = () => {
scrollableRef.current.scrollTo(scrollableRef.current.scrollHeight, { behavior: 'smooth' })
}
const handleClearRecord = useCallback(async () => {
if (!currentIdRef.current)
return
try {
await dispatch(deleteTacticsConversations([currentIdRef.current])).unwrap()
dispatch(
createTacticsConversation({
conversationData: {},
shouldNavigate: false,
shouldSendQuestion: '',
}),
)
}
catch (error) {
console.error('清除记录失败:', error)
}
}, [dispatch])
const handleReanalyze = useCallback(async () => {
// 重新触发一次提问,不携带 question
await handleSubmitQuestion(undefined, undefined, undefined, { includeQuestion: false })
// 重新拉取会话列表,保持原有行为
await dispatch(fetchTacticsConversations())
if (currentIdRef.current) {
navigate(`/tactics/chat/${currentIdRef.current}`)
}
}, [dispatch, navigate, handleSubmitQuestion])
useEffect(() => {
if (id) {
// 停止之前的请求
......@@ -311,6 +437,8 @@ export const TacticsChat: React.FC = () => {
else {
// 如果没有 id,显示欢迎语,等待登录成功后创建新会话
setAllItems([{ role: 'system' } as ChatRecord])
setHistoryDividerIndex(null)
setHistoryDividerTime(null)
setIsLoading(false)
}
}, [id, getUserQaRecordPage, dispatch])
......@@ -320,16 +448,43 @@ export const TacticsChat: React.FC = () => {
login()
}, [login])
// 进入会话后自动触发一次提交(不带 question),busiType 01 / recordType A02
const hasAutoSubmittedRef = useRef(false)
useEffect(() => {
if (currentIdRef.current && !isLoading && !hasAutoSubmittedRef.current) {
hasAutoSubmittedRef.current = true
handleSubmitQuestion(
undefined,
undefined,
undefined,
{ busiType: '01', recordType: 'A02', includeQuestion: false },
)
}
}, [isLoading, handleSubmitQuestion])
// 创建新会话成功后跳转到新会话页面
useEffect(() => {
if (shouldNavigateToNewConversation && currentConversationId) {
navigate(`/tactics/chat/${currentConversationId}`)
navigate(`/tactics/chat/${currentConversationId}`, {
state: {
shouldSendQuestion: shouldSendQuestionFromState,
tacticsMeta,
},
replace: true,
})
dispatch(clearTacticsNavigationFlag())
}
}, [shouldNavigateToNewConversation, currentConversationId, navigate, dispatch])
}, [shouldNavigateToNewConversation, currentConversationId, navigate, dispatch, shouldSendQuestionFromState, tacticsMeta])
// 处理shouldSendQuestion的变化 - 自动发送问题
useEffect(() => {
// eslint-disable-next-line no-console
console.log('[TacticsChat] autoSend check', {
shouldSendQuestion,
currentId: currentIdRef.current,
isLoading,
lastSentQuestion: lastSentQuestionRef.current,
})
if (
shouldSendQuestion
&& currentIdRef.current
......@@ -351,16 +506,31 @@ export const TacticsChat: React.FC = () => {
height: '100vh',
display: 'flex',
flexDirection: 'column',
width: '420px',
marginLeft: 'auto',
marginRight: 0,
position: 'fixed',
right: 0,
top: 0,
backgroundColor: '#FFFFFF',
zIndex: 1000,
}}
>
{/* 右上操作区 */}
<div className="flex justify-end items-center gap-[16px] px-[16px] pt-[16px] pb-[8px]">
<button
type="button"
onClick={handleClearRecord}
className="flex items-center gap-[4px] text-[#4A90E2] text-[14px] hover:opacity-80 transition-opacity cursor-pointer bg-transparent border-none outline-none"
>
<DeleteIcon className="w-[16px] h-[16px] text-[#B2B8C1]" />
<span>清除记录</span>
</button>
<button
type="button"
onClick={handleReanalyze}
className="flex items-center gap-[4px] text-[#4A90E2] text-[14px] hover:opacity-80 transition-opacity cursor-pointer bg-transparent border-none outline-none"
>
<RefreshIcon className="w-[16px] h-[16px] text-[#B2B8C1]" />
<span>重新分析</span>
</button>
</div>
<div className={`${styles.chatPage} relative flex flex-col h-full`}>
<div className={`${styles.content} flex-1 overflow-hidden flex flex-col`}>
{isLoading && (
......@@ -379,16 +549,23 @@ export const TacticsChat: React.FC = () => {
}}
className={`${styles.scrollable} scrollbar-hide scroll-smooth flex-1 overflow-y-auto px-[16px]`}
>
<div className={`${styles.inter} py-[24px]`}>
<div className={`${styles.inter} pt-[8px] pb-[24px]`}>
{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 (
<React.Fragment key={uniqueKey}>
{historyDividerIndex !== null && index === historyDividerIndex && (
<div className="flex items-center gap-[12px] my-[16px] text-[#B2B8C1] text-[12px]">
<div className="flex-1 h-px bg-[#E4E7EC]" />
<span>{historyDividerTime || formatCurrentTime()}</span>
<div className="flex-1 h-px bg-[#E4E7EC]" />
</div>
)}
<div
className="w-full chatItem mx-auto"
key={uniqueKey}
>
{record.role === 'system' && <TacticsWelcome />}
{record.role === 'user' && <ChatItemUser record={record} />}
......@@ -402,6 +579,7 @@ export const TacticsChat: React.FC = () => {
/>
)}
</div>
</React.Fragment>
)
})}
</div>
......@@ -425,10 +603,22 @@ export const TacticsChat: React.FC = () => {
</Button>
</motion.div>
</div>
<ChatEditor
onSubmit={handleSubmitQuestion}
placeholders={[]}
hideTools
<TacticsChatEditor
onSubmit={(question) => {
// eslint-disable-next-line no-console
console.log('[TacticsChat] onSubmit wrapper', {
question,
currentId: currentIdRef.current,
isLoading,
})
return handleSubmitQuestion(
question,
undefined,
undefined,
{ busiType: '02', recordType: 'A01', includeQuestion: true },
)
}}
placeholders={['']}
/>
<div className="w-full text-center mt-[12px] text-[#3333334d] text-[12px]">
内容由AI模型生成,其准确性和完整性无法保证,仅供参考
......
// 问答功能独立首页
import type React from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { useLocalStorageState } from 'ahooks'
import styles from '../Home/Home.module.less'
import { TacticsWelcome } from './components/TacticsWelcome'
import { clearTacticsNavigationFlag, createTacticsConversation, deleteTacticsConversations, fetchTacticsConversations } from '@/store/tacticsSlice'
import { clearTacticsNavigationFlag, createTacticsConversation, fetchTacticsConversations } from '@/store/tacticsSlice'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import type { RootState } from '@/store'
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'
import DeleteIcon from '@/assets/svg/delete.svg?react'
import RefreshIcon from '@/assets/svg/refresh.svg?react'
import { TacticsChatEditor } from '@/components/TacticsChatEditor'
export const TacticsHome: React.FC = () => {
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
const dispatch = useAppDispatch()
const location = useLocation()
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const hasFetched = useRef(false)
const { shouldNavigateToNewConversation, currentConversationId, shouldSendQuestion } = useAppSelector((state: RootState) => state.tactics)
const [token, setToken] = useLocalStorageState<string | undefined>('__TOKEN__', {
defaultValue: '',
})
const abortControllerRef = useRef<AbortController | null>(null)
// 仅在 from=tactics 且 version=2 时读取并缓存额外参数
const tacticsMetaFromSearch = useMemo(() => {
const from = searchParams.get('from')
const version = searchParams.get('version')
if (from !== 'tactics' || version !== '2') {
return undefined
}
return {
version,
taskId: searchParams.get('taskId') || undefined,
pinBeginTime: searchParams.get('pinBeginTime') || undefined,
pinEndTime: searchParams.get('pinEndTime') || undefined,
partOrAll: searchParams.get('partOrAll') || 'all',
channel: searchParams.get('channel') || 'AIphone',
channelName: searchParams.get('channelName') || undefined,
}
}, [searchParams])
const tacticsMetaFromStorage = useMemo(() => {
const raw = sessionStorage.getItem('tacticsMeta')
if (!raw) {
return undefined
}
try {
const parsed = JSON.parse(raw)
if (parsed?.from === 'tactics' && parsed?.version === '2') {
return parsed
}
}
catch {
// ignore
}
return undefined
}, [])
const tacticsMeta = useMemo(() => tacticsMetaFromSearch || tacticsMetaFromStorage, [tacticsMetaFromSearch, tacticsMetaFromStorage])
// 同步到 sessionStorage,便于跳转后读取;仅在有 meta 时写入,避免覆盖重定向缓存
useEffect(() => {
if (tacticsMeta) {
sessionStorage.setItem('tacticsMeta', JSON.stringify(tacticsMeta))
}
}, [tacticsMeta])
const initTacticsConversation = () => {
const fromCollect = location.state?.fromCollect
......@@ -51,47 +89,12 @@ export const TacticsHome: React.FC = () => {
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,
},
() => {
// 在首页不需要处理消息,跳转到聊天页面后会自动刷新加载最新消息
navigate(`/tactics/chat/${currentConversationId}`, {
state: {
shouldSendQuestion: question,
tacticsMeta,
},
abortControllerRef.current.signal,
)
// 跳转到聊天页面查看结果
navigate(`/tactics/chat/${currentConversationId}`)
})
return
}
// 如果没有会话,才创建新会话
......@@ -102,7 +105,7 @@ export const TacticsHome: React.FC = () => {
shouldSendQuestion: question,
}),
)
}, [dispatch, currentConversationId, navigate])
}, [dispatch, currentConversationId, navigate, tacticsMeta])
// 监听导航到新对话
useEffect(() => {
......@@ -111,11 +114,13 @@ export const TacticsHome: React.FC = () => {
navigate(url, {
state: {
shouldSendQuestion,
tacticsMeta,
},
replace: true,
})
dispatch(clearTacticsNavigationFlag())
}
}, [shouldNavigateToNewConversation, currentConversationId, shouldSendQuestion, navigate, dispatch])
}, [shouldNavigateToNewConversation, currentConversationId, shouldSendQuestion, navigate, dispatch, tacticsMeta])
const login = useCallback(async () => {
if (hasFetched.current) {
......@@ -178,35 +183,6 @@ export const TacticsHome: React.FC = () => {
}, [])
// 处理清除记录
const handleClearRecord = useCallback(async () => {
if (!currentConversationId) {
return
}
try {
await dispatch(deleteTacticsConversations([currentConversationId])).unwrap()
// 清除后创建新会话
dispatch(
createTacticsConversation({
conversationData: {},
shouldNavigate: false,
shouldSendQuestion: '',
}),
)
}
catch (error) {
console.error('清除记录失败:', error)
}
}, [currentConversationId, dispatch])
// 处理重新分析
const handleReanalyze = useCallback(async () => {
// 重新获取会话列表
await dispatch(fetchTacticsConversations())
// 如果有当前会话,可以重新加载
if (currentConversationId) {
navigate(`/tactics/chat/${currentConversationId}`)
}
}, [currentConversationId, dispatch, navigate])
return (
<div
......@@ -215,36 +191,12 @@ export const TacticsHome: React.FC = () => {
height: '100vh',
display: 'flex',
flexDirection: 'column',
width: '420px',
marginLeft: 'auto',
marginRight: 0,
position: 'fixed',
right: 0,
top: 0,
backgroundColor: '#FFFFFF',
zIndex: 1000,
}}
>
{/* 按钮区域 - 右侧展示 */}
<div className="flex justify-end items-center gap-[16px] px-[16px] pt-[16px] pb-[8px]">
<button
type="button"
onClick={handleClearRecord}
className="flex items-center gap-[4px] text-[#4A90E2] text-[14px] hover:opacity-80 transition-opacity cursor-pointer bg-transparent border-none outline-none"
>
<DeleteIcon className="w-[16px] h-[16px] text-[#B2B8C1]" />
<span>清除记录</span>
</button>
<button
type="button"
onClick={handleReanalyze}
className="flex items-center gap-[4px] text-[#4A90E2] text-[14px] hover:opacity-80 transition-opacity cursor-pointer bg-transparent border-none outline-none"
>
<RefreshIcon className="w-[16px] h-[16px] text-[#B2B8C1]" />
<span>重新分析</span>
</button>
</div>
<div className="h-full w-full flex flex-col">
{/* 主要内容区域 - 全屏显示 */}
<div className="flex-1 overflow-hidden flex flex-col">
......@@ -254,11 +206,9 @@ export const TacticsHome: React.FC = () => {
</div>
{/* 底部输入框 */}
<div className="box-border px-[16px] pb-[18px] pt-[12px] bg-white border-t border-gray-100">
<ChatEditor
showContentTips
<TacticsChatEditor
onSubmit={handleCreateConversation}
placeholders={RECOMMEND_QUESTIONS_OTHER}
hideTools
placeholders={['']}
/>
<div className="w-full text-center mt-[12px] text-[#3333334d] text-[12px]">
内容由AI模型生成,其准确性和完整性无法保证,仅供参考
......
......@@ -10,7 +10,7 @@ export const TacticsWelcome: React.FC = () => {
return (
<div className="chatWelcomeContainer w-full">
<div className="h-[20px] sm:h-[32px] w-full"></div>
<div className="h-[8px] sm:h-[16px] w-full"></div>
<div className="flex items-start">
<Avatar className="mr-[12px] flex-shrink-0" src={viteOutputObj === 'inner' ? AIIcon : AvatarBot} />
<motion.div
......@@ -27,7 +27,7 @@ export const TacticsWelcome: React.FC = () => {
</div>
</motion.div>
</div>
<div className="h-[20px] sm:h-[32px] w-full"></div>
<div className="h-[12px] sm:h-[24px] w-full"></div>
</div>
)
}
......@@ -15,9 +15,8 @@ const AppRoutesComponent: React.FC = () => {
<Route path="/chat/:id" element={<Chat />} />
</Route>
<Route path="/home" element={<Home />}></Route>
<Route path="/tactics" element={<TacticsHome />}>
<Route path="/tactics" element={<TacticsHome />} />
<Route path="/tactics/chat/:id" element={<TacticsChat />} />
</Route>
<Route path="/collect" element={<Collect />} />
<Route path="/tools" element={<Tools />} />
<Route path="/protocol/:id" element={<Protocol />} />
......
......@@ -23,6 +23,23 @@ export function withRouteChangeHandler(WrappedComponent: React.ComponentType) {
const searchParams = new URLSearchParams(location.search)
const from = searchParams.get('from')
if (from === 'tactics') {
// 在重定向前缓存查询参数,供 /tactics 页读取(避免 from 被清理导致丢失)
try {
const meta = {
version: searchParams.get('version') || undefined,
taskId: searchParams.get('taskId') || undefined,
pinBeginTime: searchParams.get('pinBeginTime') || undefined,
pinEndTime: searchParams.get('pinEndTime') || undefined,
partOrAll: searchParams.get('partOrAll') || 'all',
channel: searchParams.get('channel') || 'AIphone',
channelName: searchParams.get('channelName') || undefined,
from,
}
sessionStorage.setItem('tacticsMeta', JSON.stringify(meta))
}
catch {
// ignore
}
// 重定向到 /tactics,保留其他查询参数
const newSearchParams = new URLSearchParams(location.search)
newSearchParams.delete('from')
......
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