Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
sdream-ai-fe
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
侯明涛
sdream-ai-fe
Commits
94a7e78b
Commit
94a7e78b
authored
Dec 09, 2025
by
Liu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix:kuan
parent
dee25152
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
249 additions
and
166 deletions
+249
-166
src/pages/ChatTactics/Chat.tsx
+241
-163
src/pages/Home/HomeNew.tsx
+8
-3
No files found.
src/pages/ChatTactics/Chat.tsx
View file @
94a7e78b
...
...
@@ -22,6 +22,9 @@ import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
import
{
setIsAsking
}
from
'@/store/chatSlice'
import
SdreamLoading
from
'@/components/SdreamLoading'
// 记录已自动触发提问的会话,避免严格模式或多次渲染造成重复调用
const
autoSubmittedConversationIds
=
new
Set
<
string
>
()
function
formatDateTime
(
date
:
Date
)
{
const
pad
=
(
num
:
number
)
=>
num
.
toString
().
padStart
(
2
,
'0'
)
const
year
=
date
.
getFullYear
()
...
...
@@ -80,6 +83,8 @@ export const Chat: React.FC = () => {
const
[
hasHistoryRecord
,
setHasHistoryRecord
]
=
useState
(
false
)
const
[
historyItemsCount
,
setHistoryItemsCount
]
=
useState
(
0
)
const
[
historyTimestamp
,
setHistoryTimestamp
]
=
useState
(
''
)
// 当历史记录需要追加到新消息之后时,用于确定分隔线插入位置(分隔线插在 historyDividerIndex 前一个元素之后)
const
[
historyDividerIndex
,
setHistoryDividerIndex
]
=
useState
<
number
|
null
>
(
null
)
const
dispatch
=
useAppDispatch
()
const
{
shouldSendQuestion
,
currentToolId
,
conversations
}
=
useAppSelector
((
state
:
RootState
)
=>
state
.
conversation
)
const
scrollableRef
=
useRef
<
HTMLDivElement
|
any
>
(
null
)
...
...
@@ -95,6 +100,8 @@ export const Chat: React.FC = () => {
const
isFromTactics
=
searchParams
.
get
(
'from'
)
===
'tactics'
const
[
showClearConfirm
,
setShowClearConfirm
]
=
useState
(
false
)
const
[
isClearingHistory
,
setIsClearingHistory
]
=
useState
(
false
)
// 是否需要在自动提问完成后再拼接历史记录
const
shouldAppendHistoryAfterAutoRef
=
useRef
(
false
)
// 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时,
// 视为一次新的会话入口:重置为通用模式,清除历史遗留的工具模式状态
...
...
@@ -197,9 +204,23 @@ export const Chat: React.FC = () => {
/** 处理正常stream的数据 */
const
handleStreamMesageData
=
(
msg
:
any
,
question
:
string
)
=>
{
const
resolvedQuestion
=
question
||
msg
?.
content
?.
data
?.
question
||
''
setAllItems
((
prevItems
)
=>
{
const
newItems
=
[...
prevItems
]
// 创建数组的浅拷贝
const
lastIndex
=
newItems
.
length
-
1
const
prevIndex
=
lastIndex
-
1
// 如果自动提问时后端返回了具体的问题,补全上一条用户问题
if
(
resolvedQuestion
&&
prevIndex
>=
0
&&
newItems
[
prevIndex
].
role
===
'user'
&&
!
newItems
[
prevIndex
].
question
)
{
newItems
[
prevIndex
]
=
{
...
newItems
[
prevIndex
],
question
:
resolvedQuestion
,
}
}
if
(
lastIndex
>=
0
)
{
// 创建最后一项的新对象,合并现有数据和新的 answer
const
originalAnswer
=
(
newItems
[
lastIndex
].
answerList
?.[
0
]?.
answer
||
''
)
+
msg
.
content
.
data
.
answer
...
...
@@ -219,8 +240,8 @@ export const Chat: React.FC = () => {
],
}
// 只有当question不为空时才设置question字段
if
(
q
uestion
)
{
updatedItem
.
question
=
q
uestion
if
(
resolvedQ
uestion
)
{
updatedItem
.
question
=
resolvedQ
uestion
}
newItems
[
lastIndex
]
=
updatedItem
}
...
...
@@ -230,6 +251,7 @@ export const Chat: React.FC = () => {
/** 处理超过最大条数的数据 */
const
handleChatMaxCount
=
(
msg
:
any
,
question
:
string
)
=>
{
const
resolvedQuestion
=
question
||
msg
?.
content
?.
data
?.
question
||
''
// toast(t => (
// <div className="flex items-center">
// <p className="text-[14px]">⚠️ 超过最大轮数上限!请新建对话 👉🏻</p>
...
...
@@ -275,8 +297,8 @@ export const Chat: React.FC = () => {
],
}
// 只有当question不为空时才设置question字段
if
(
q
uestion
)
{
updatedItem
.
question
=
q
uestion
if
(
resolvedQ
uestion
)
{
updatedItem
.
question
=
resolvedQ
uestion
}
newItems
[
lastIndex
]
=
updatedItem
}
...
...
@@ -284,144 +306,20 @@ export const Chat: React.FC = () => {
})
}
/** 提交问题 */
const
handleSubmitQuestion
=
async
(
question
:
string
,
productCode
?:
string
,
toolId
?:
string
,
isAutoCall
?:
boolean
)
=>
{
const
resolvedToolId
=
toolId
??
currentToolId
??
undefined
// 根据是否是自动调用设置不同的参数
const
busiType
=
isAutoCall
?
'01'
:
'02'
const
recordType
=
isAutoCall
?
'A02'
:
'A01'
// 停止之前的请求
if
(
abortControllerRef
.
current
)
{
abortControllerRef
.
current
.
abort
()
}
const
isNew
=
allItems
.
length
<=
1
dispatch
(
setIsAsking
(
true
))
// 检查token
await
fetchCheckTokenApi
()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
if
(
question
)
{
// 一次性添加用户问题和空的AI回答
setAllItems
(
prevItems
=>
[
...
prevItems
,
{
role
:
'user'
,
question
,
}
as
ChatRecord
,
{
role
:
'ai'
,
answerList
:
[{
answer
:
''
}],
}
as
ChatRecord
,
])
}
else
{
// 自动调用时,只添加空的AI回答
setAllItems
(
prevItems
=>
[
...
prevItems
,
{
role
:
'ai'
,
answerList
:
[{
answer
:
''
}],
}
as
ChatRecord
,
])
}
// 创建新的 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
// 构建请求参数,包含路由参数
const
requestParams
:
Record
<
string
,
any
>
=
{
question
,
conversationId
:
currentIdRef
.
current
,
stream
:
true
,
productCode
,
toolId
:
resolvedToolId
,
busiType
,
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
)
=>
{
// 检查是否已被取消
if
(
abortControllerRef
.
current
?.
signal
.
aborted
)
{
return
}
// 处理错误
if
(
msg
?.
type
===
'ERROR'
)
{
// 如果是 AbortError,不显示错误
if
(
msg
.
content
?.
name
===
'AbortError'
)
{
return
}
return
}
// 正常的stream数据
if
(
msg
?.
type
===
'DATA'
&&
msg
?.
content
?.
code
===
'00000000'
)
{
handleStreamMesageData
(
msg
,
question
)
}
if
(
msg
?.
type
===
'DATA'
&&
msg
?.
content
?.
code
===
'01010005'
)
{
handleChatMaxCount
(
msg
,
question
)
return
}
if
(
msg
.
type
===
'END'
)
{
if
(
isNew
)
{
setTimeout
(()
=>
{
dispatch
(
fetchConversations
())
},
2000
)
}
}
},
abortControllerRef
.
current
.
signal
,
)
}
/** 获取qa记录 */
const
getUserQaRecordPage
=
useCallback
(
async
(
conversationId
:
string
)
=>
{
setIsLoading
(
true
)
const
getUserQaRecordPage
=
useCallback
(
async
(
conversationId
:
string
,
options
?:
{
append
?:
boolean
,
showLoading
?:
boolean
},
)
=>
{
const
append
=
options
?.
append
??
false
const
showLoading
=
options
?.
showLoading
??
!
append
if
(
showLoading
)
setIsLoading
(
true
)
setHasHistoryRecord
(
false
)
setHistoryItemsCount
(
0
)
setHistoryTimestamp
(
''
)
if
(
!
append
)
setHistoryDividerIndex
(
null
)
try
{
// 检测是否从收藏页返回
const
fromCollect
=
location
.
state
?.
fromCollect
...
...
@@ -447,9 +345,32 @@ export const Chat: React.FC = () => {
}
return
item
})
setAllItems
(
processedMessages
)
const
messagesWithoutSystemForAppend
=
append
?
processedMessages
.
filter
((
_
,
idx
)
=>
!
(
idx
===
0
&&
processedMessages
[
idx
].
role
===
'system'
))
:
processedMessages
if
(
append
)
{
if
(
!
messagesWithoutSystemForAppend
.
length
)
{
setHistoryDividerIndex
(
null
)
}
setAllItems
((
prev
)
=>
{
const
baseLength
=
prev
.
length
const
historyLength
=
messagesWithoutSystemForAppend
.
length
if
(
historyLength
)
{
// 分割线放在历史记录之后
setHistoryDividerIndex
(
baseLength
+
historyLength
-
1
)
}
return
[
...
prev
,
...
messagesWithoutSystemForAppend
,
]
})
}
else
{
setAllItems
(
messagesWithoutSystemForAppend
)
}
setHasHistoryRecord
(
hasQaRecords
)
setHistoryItemsCount
(
hasQaRecords
?
processedMessages
.
length
:
0
)
setHistoryItemsCount
(
hasQaRecords
?
messagesWithoutSystemForAppend
.
length
:
0
)
if
(
hasQaRecords
)
{
setHistoryTimestamp
(
formatDateTime
(
new
Date
()))
}
...
...
@@ -566,10 +487,151 @@ export const Chat: React.FC = () => {
// 错误处理
}
finally
{
setIsLoading
(
false
)
if
(
showLoading
)
setIsLoading
(
false
)
}
},
[
dispatch
,
currentToolId
,
conversations
,
location
.
state
])
/** 提交问题 */
const
handleSubmitQuestion
=
async
(
question
:
string
,
productCode
?:
string
,
toolId
?:
string
,
isAutoCall
?:
boolean
)
=>
{
const
resolvedToolId
=
toolId
??
currentToolId
??
undefined
// 根据是否是自动调用设置不同的参数
const
busiType
=
isAutoCall
?
'01'
:
'02'
const
recordType
=
isAutoCall
?
'A02'
:
'A01'
// 停止之前的请求
if
(
abortControllerRef
.
current
)
{
abortControllerRef
.
current
.
abort
()
}
const
isNew
=
allItems
.
length
<=
1
dispatch
(
setIsAsking
(
true
))
// 检查token
await
fetchCheckTokenApi
()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
// 自动调用也先插入一条用户问题占位,待后端返回真实问题后补全
setAllItems
(
prevItems
=>
[
...
prevItems
,
{
role
:
'user'
,
question
,
}
as
ChatRecord
,
{
role
:
'ai'
,
answerList
:
[{
answer
:
''
}],
}
as
ChatRecord
,
])
// 创建新的 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
// 构建请求参数,包含路由参数
const
requestParams
:
Record
<
string
,
any
>
=
{
conversationId
:
currentIdRef
.
current
,
stream
:
true
,
productCode
,
toolId
:
resolvedToolId
,
busiType
,
recordType
,
}
// 自动调用不传 question;手动调用正常传递
if
(
!
isAutoCall
)
{
requestParams
.
question
=
question
}
// 添加路由参数(如果存在)
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
)
=>
{
// 检查是否已被取消
if
(
abortControllerRef
.
current
?.
signal
.
aborted
)
{
return
}
// 处理错误
if
(
msg
?.
type
===
'ERROR'
)
{
// 如果是 AbortError,不显示错误
if
(
msg
.
content
?.
name
===
'AbortError'
)
{
return
}
return
}
// 正常的stream数据
if
(
msg
?.
type
===
'DATA'
&&
msg
?.
content
?.
code
===
'00000000'
)
{
handleStreamMesageData
(
msg
,
question
)
}
if
(
msg
?.
type
===
'DATA'
&&
msg
?.
content
?.
code
===
'01010005'
)
{
handleChatMaxCount
(
msg
,
question
)
return
}
if
(
msg
.
type
===
'END'
)
{
if
(
isNew
)
{
setTimeout
(()
=>
{
dispatch
(
fetchConversations
())
},
2000
)
}
// 自动提问完成后,再追加历史记录
if
(
isAutoCall
&&
shouldAppendHistoryAfterAutoRef
.
current
&&
currentIdRef
.
current
)
{
shouldAppendHistoryAfterAutoRef
.
current
=
false
getUserQaRecordPage
(
currentIdRef
.
current
,
{
append
:
true
,
showLoading
:
false
})
}
}
},
abortControllerRef
.
current
.
signal
,
)
}
/**
* 战术入口的自动提问(进入页面和“重新分析”按钮公用)
* 保证每次都按自动模式调用,不携带 question 参数
*/
const
triggerAutoSubmit
=
useCallback
(()
=>
{
if
(
!
currentIdRef
.
current
||
isLoading
)
return
// 自动调用后重新追加最新历史
shouldAppendHistoryAfterAutoRef
.
current
=
true
handleSubmitQuestion
(
''
,
undefined
,
currentToolId
,
true
)
},
[
handleSubmitQuestion
,
isLoading
,
currentToolId
])
/** 点击滚动到底部 */
const
scrollToBottom
=
()
=>
{
scrollableRef
.
current
.
scrollTo
(
scrollableRef
.
current
.
scrollHeight
,
{
behavior
:
'smooth'
})
...
...
@@ -586,9 +648,21 @@ export const Chat: React.FC = () => {
currentIdRef
.
current
=
id
lastSentQuestionRef
.
current
=
''
// 重置标记
hasAutoSubmittedRef
.
current
=
false
// 重置自动提交标记
getUserQaRecordPage
(
id
)
shouldAppendHistoryAfterAutoRef
.
current
=
isFromTactics
// 初始化为仅包含 system 的列表,等待自动提问或历史拉取
setAllItems
([{
role
:
'system'
}
as
ChatRecord
])
setHasHistoryRecord
(
false
)
setHistoryItemsCount
(
0
)
setHistoryTimestamp
(
''
)
setHistoryDividerIndex
(
null
)
// 非战术入口:立即拉取历史(保持原行为)
if
(
!
isFromTactics
)
{
getUserQaRecordPage
(
id
)
}
}
},
[
id
])
},
[
id
,
isFromTactics
,
getUserQaRecordPage
,
dispatch
])
// 处理shouldSendQuestion的变化 - 自动发送问题
useEffect
(()
=>
{
...
...
@@ -608,34 +682,37 @@ export const Chat: React.FC = () => {
}
},
[
shouldSendQuestion
,
isLoading
,
currentToolId
])
// 页面加载时自动调用
submit接口
// 页面加载时自动调用
submit 接口(战术入口只触发一次)
useEffect
(()
=>
{
if
(
currentIdRef
.
current
&&
!
isLoading
&&
!
hasAutoSubmittedRef
.
current
&&
isFromTactics
)
{
hasAutoSubmittedRef
.
current
=
true
// 确保历史记录加载完成后再调用submit接口
setTimeout
(()
=>
{
handleSubmitQuestion
(
''
,
undefined
,
currentToolId
,
true
)
// 自动调用,传递 isAutoCall = true
},
100
)
}
},
[
isLoading
,
currentToolId
,
isFromTactics
])
if
(
!
isFromTactics
)
return
const
conversationId
=
currentIdRef
.
current
// 确保存在会话且未处于加载中
if
(
!
conversationId
||
isLoading
)
return
// 避免严格模式或多次渲染导致重复自动提交
if
(
autoSubmittedConversationIds
.
has
(
conversationId
))
return
autoSubmittedConversationIds
.
add
(
conversationId
)
hasAutoSubmittedRef
.
current
=
true
// 确保历史记录加载完成后再调用submit接口
setTimeout
(()
=>
{
triggerAutoSubmit
()
// 自动调用,传递 isAutoCall = true
},
100
)
},
[
isLoading
,
isFromTactics
,
triggerAutoSubmit
])
// 监听“重新分析”事件,重新发起一次自动提问(仍使用路由参数)
useEffect
(()
=>
{
const
handleReAnalyze
=
()
=>
{
if
(
currentIdRef
.
current
&&
!
isLoading
)
{
handleSubmitQuestion
(
''
,
undefined
,
currentToolId
,
true
)
}
triggerAutoSubmit
()
}
window
.
addEventListener
(
'tacticsReAnalyze'
,
handleReAnalyze
as
EventListener
)
return
()
=>
{
window
.
removeEventListener
(
'tacticsReAnalyze'
,
handleReAnalyze
as
EventListener
)
}
},
[
isLoading
,
currentToolId
])
},
[
triggerAutoSubmit
])
// 根据 currentToolId 获取对应的 toolName
useEffect
(()
=>
{
...
...
@@ -775,9 +852,10 @@ export const Chat: React.FC = () => {
const
uniqueKey
=
recordId
?
`${record.role}-${recordId}`
:
`${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}`
const
shouldShowHistoryDivider
=
hasHistoryRecord
const
shouldShowHistoryDivider
=
historyDividerIndex
!==
null
&&
hasHistoryRecord
&&
historyItemsCount
>
0
&&
index
===
history
ItemsCount
-
1
&&
index
===
history
DividerIndex
const
historyDividerText
=
`以上为历史分析数据 ${historyTimestamp || formatDateTime(new Date())}`
return
(
<
React
.
Fragment
key=
{
uniqueKey
}
>
...
...
src/pages/Home/HomeNew.tsx
View file @
94a7e78b
...
...
@@ -81,7 +81,9 @@ export const Home: React.FC = () => {
}
// 处理工具按钮点击
const
requestIdRef
=
useRef
(
0
)
// 标记最新请求,避免旧响应覆盖
const
_handleToolClick
=
useCallback
(
async
(
isToolBtn
:
boolean
,
toolId
?:
string
,
ignoreUrlToolId
?:
boolean
)
=>
{
const
currentRequestId
=
++
requestIdRef
.
current
// 提质增效模式 / 数据助手 / 通用模式:都先清空数据,重新拉常见问题
setOtherQuestions
((
prev
:
any
)
=>
({
...
prev
,
...
...
@@ -115,7 +117,8 @@ export const Home: React.FC = () => {
const
res
=
await
fetchEfficiencyQuestionList
({
toolId
:
sessionToolId
||
finalToolId
,
})
if
(
res
&&
res
.
data
&&
res
.
data
.
questions
)
{
// 只接受当前最新的请求结果,避免旧请求覆盖新请求
if
(
currentRequestId
===
requestIdRef
.
current
&&
res
?.
data
?.
questions
)
{
setOtherQuestions
((
prev
:
any
)
=>
({
...
prev
,
content
:
res
.
data
.
questions
||
[],
...
...
@@ -126,7 +129,9 @@ export const Home: React.FC = () => {
console
.
error
(
'获取工具相关问题失败:'
,
error
)
}
finally
{
setIsDataLoaded
(
true
)
// 无论成功失败都标记为已加载
// 仅在当前请求仍是最新时更新加载态,避免闪烁
if
(
currentRequestId
===
requestIdRef
.
current
)
setIsDataLoaded
(
true
)
}
},
[
originalOtherQuestions
,
location
.
search
])
...
...
@@ -329,7 +334,7 @@ export const Home: React.FC = () => {
<
div
className=
"hidden sm:flex h-full flex-none ml-auto"
>
<
div
className=
"w-full h-full bg-transparent box-border rounded-[24px]"
style=
{
{
width
:
'420px'
,
height
:
'calc(100vh - 64px)'
,
background
:
'#FFFFFF'
,
padding
:
'0 30px'
}
}
style=
{
{
height
:
'calc(100vh - 64px)'
,
background
:
'#FFFFFF'
,
padding
:
'0 30px'
}
}
>
<
Outlet
/>
</
div
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment