Commit 2115e7da by Liu

feat(chat): 工作流卡片字段、会话状态与聊天页联动

- 新增 CardWorkflowField,调整 CardWorkflowForm 与附件展示
- 扩展 conversation/chat 类型与 conversationSlice 业务洞察工作流状态
- Chat 与 HomeNew 对接工作流提交与参数逻辑

Co-authored-by: Cursor <cursoragent@cursor.com>
parent 69439c2d
...@@ -14,8 +14,9 @@ import type { ChatRecord } from '@/types/chat' ...@@ -14,8 +14,9 @@ import type { ChatRecord } from '@/types/chat'
import { fetchSessionConversationId, fetchUserQaRecordPage } from '@/api/conversation' import { fetchSessionConversationId, fetchUserQaRecordPage } from '@/api/conversation'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat' import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
import { fetchToolList } from '@/api/home' import { fetchToolList } from '@/api/home'
import { clearCurrentToolId, clearShouldSendQuestion, fetchConversations, setCurrentToolId } from '@/store/conversationSlice' import { clearBusinessInsightWorkflow, clearCurrentToolId, clearShouldSendQuestion, exitBusinessInsightSubWorkflow, fetchConversations, setBusinessInsightMainWorkflow, setBusinessInsightPendingQuestion, setBusinessInsightSubWorkflow, setCurrentToolId, updateBusinessInsightParamMap } from '@/store/conversationSlice'
import { getUserRolesForApi, safeSessionStorageGetItem, waitForToken } from '@/lib/utils' import { getUserRolesForApi, safeSessionStorageGetItem, waitForToken } from '@/lib/utils'
import { getWorkflowToolIdAndRecordTypeForSubmit, setWorkflowLastToolPair } from '@/lib/workflowToolParams'
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'
...@@ -48,7 +49,7 @@ export const Chat: React.FC = () => { ...@@ -48,7 +49,7 @@ export const Chat: React.FC = () => {
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()
const { shouldSendQuestion, currentToolId, conversations } = useAppSelector((state: RootState) => state.conversation) const { shouldSendQuestion, currentToolId, conversations, businessInsightWorkflow } = useAppSelector((state: RootState) => state.conversation)
const isAsking = useAppSelector((state: RootState) => state.chat.isAsking) const isAsking = useAppSelector((state: RootState) => state.chat.isAsking)
// 标记历史记录是否已加载完成,用于确保自动发送问题在历史记录加载后执行 // 标记历史记录是否已加载完成,用于确保自动发送问题在历史记录加载后执行
const historyLoadedRef = useRef<{ conversationId: string | null }>({ conversationId: null }) const historyLoadedRef = useRef<{ conversationId: string | null }>({ conversationId: null })
...@@ -75,6 +76,22 @@ export const Chat: React.FC = () => { ...@@ -75,6 +76,22 @@ export const Chat: React.FC = () => {
// 提质增效自动调用欢迎语候选(用于流结束后写入 sessionStorage) // 提质增效自动调用欢迎语候选(用于流结束后写入 sessionStorage)
const autoWelcomeCandidateRef = useRef<string>('') const autoWelcomeCandidateRef = useRef<string>('')
const hasAutoWelcomeCandidateRef = useRef<boolean>(false) const hasAutoWelcomeCandidateRef = useRef<boolean>(false)
// 业务洞察工作流状态 ref(用于在流结束后访问最新状态)
const businessInsightWorkflowRef = useRef<{
mainWorkflowSessionId: string | null
subWorkflowSessionId: string | null
subWorkflowRecordType: string | null
paramMap: Record<string, string>
isInSubWorkflow: boolean
pendingQuestion: string | null
}>({
mainWorkflowSessionId: null,
subWorkflowSessionId: null,
subWorkflowRecordType: null,
paramMap: {},
isInSubWorkflow: false,
pendingQuestion: null,
})
// 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时, // 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时,
// 视为一次新的会话入口:重置为制度活化,清除历史遗留的工具模式状态 // 视为一次新的会话入口:重置为制度活化,清除历史遗留的工具模式状态
...@@ -161,6 +178,11 @@ export const Chat: React.FC = () => { ...@@ -161,6 +178,11 @@ export const Chat: React.FC = () => {
} }
}, [dispatch, initialToolId]) }, [dispatch, initialToolId])
// 同步业务洞察工作流状态到 ref
useEffect(() => {
businessInsightWorkflowRef.current = businessInsightWorkflow
}, [businessInsightWorkflow])
/** 处理正常stream的数据 */ /** 处理正常stream的数据 */
const handleStreamMesageData = (msg: any, question: string) => { const handleStreamMesageData = (msg: any, question: string) => {
setAllItems((prevItems) => { setAllItems((prevItems) => {
...@@ -231,6 +253,10 @@ export const Chat: React.FC = () => { ...@@ -231,6 +253,10 @@ export const Chat: React.FC = () => {
// 在流式响应过程中,忽略服务器返回的 endAnswerFlag,避免过早禁用输入框 // 在流式响应过程中,忽略服务器返回的 endAnswerFlag,避免过早禁用输入框
const { endAnswerFlag: _omitEndAnswerFlag, ...dataWithoutEndFlag } = msg.content.data || {} const { endAnswerFlag: _omitEndAnswerFlag, ...dataWithoutEndFlag } = msg.content.data || {}
const chunkToolId = dataWithoutEndFlag.toolId != null ? String(dataWithoutEndFlag.toolId).trim() : ''
const chunkRecordType = dataWithoutEndFlag.recordType != null ? String(dataWithoutEndFlag.recordType).trim() : ''
if (chunkToolId && chunkRecordType)
setWorkflowLastToolPair(chunkToolId, chunkRecordType)
// 保留之前保存的 workFlowSessionId 和 recordId,避免被覆盖 // 保留之前保存的 workFlowSessionId 和 recordId,避免被覆盖
newItems[currentIndex] = { newItems[currentIndex] = {
...newItems[currentIndex], ...newItems[currentIndex],
...@@ -302,10 +328,29 @@ export const Chat: React.FC = () => { ...@@ -302,10 +328,29 @@ export const Chat: React.FC = () => {
// 优先读取缓存中的 toolId,再回退到传参或 Redux // 优先读取缓存中的 toolId,再回退到传参或 Redux
const sessionToolId = sessionStorage.getItem('currentToolId') ?? undefined const sessionToolId = sessionStorage.getItem('currentToolId') ?? undefined
const resolvedToolId = toolId ?? sessionToolId ?? currentToolId ?? undefined const resolvedToolId = toolId ?? sessionToolId ?? currentToolId ?? undefined
// 判断是否为提质增效模式的自动调用(空问题) // 业务洞察等工作流参数:有主/子会话态时合并进 extra(不依赖写死 toolId)
let workflowExtraParams = extraParams
if (businessInsightWorkflowRef.current) {
const workflowState = businessInsightWorkflowRef.current
if (workflowState.isInSubWorkflow) {
workflowExtraParams = {
...extraParams,
recordType: workflowState.subWorkflowRecordType || undefined,
workFlowSessionId: workflowState.subWorkflowSessionId || workFlowSessionId,
paramMap: workflowState.paramMap,
}
}
else if (workflowState.mainWorkflowSessionId) {
workflowExtraParams = {
...extraParams,
workFlowSessionId: workflowState.mainWorkflowSessionId,
}
}
}
// 空问题 + 显式传入 recordType/busiType(如工具入口 autoSubmit)时走自动欢迎语流式逻辑
const isEmptyQuestion = !question.trim() const isEmptyQuestion = !question.trim()
const isQualityImprovement = resolvedToolId === '6712395743241' const isAutoToolEmptySubmit = isEmptyQuestion && Boolean(extraParams?.recordType && extraParams?.busiType)
isAutoSubmitQualityImprovementRef.current = isEmptyQuestion && isQualityImprovement isAutoSubmitQualityImprovementRef.current = isAutoToolEmptySubmit
if (isAutoSubmitQualityImprovementRef.current) { if (isAutoSubmitQualityImprovementRef.current) {
// 重置欢迎语候选 // 重置欢迎语候选
autoWelcomeCandidateRef.current = '' autoWelcomeCandidateRef.current = ''
...@@ -366,12 +411,12 @@ export const Chat: React.FC = () => { ...@@ -366,12 +411,12 @@ export const Chat: React.FC = () => {
} }
// 如果是 workflow 提交,添加 paramMap、recordId 和 workFlowSessionId // 如果是 workflow 提交,添加 paramMap、recordId 和 workFlowSessionId
if (extraParams?.paramMap) { if (workflowExtraParams?.paramMap) {
requestBody.paramMap = extraParams.paramMap requestBody.paramMap = workflowExtraParams.paramMap
} }
// 优先使用 extraParams 中的值,其次使用直接传递的参数 // 优先使用 workflowExtraParams 中的值,其次使用直接传递的参数
if (extraParams?.recordId) { if (workflowExtraParams?.recordId) {
requestBody.recordId = extraParams.recordId requestBody.recordId = workflowExtraParams.recordId
} }
else if (recordId) { else if (recordId) {
requestBody.recordId = recordId requestBody.recordId = recordId
...@@ -380,17 +425,16 @@ export const Chat: React.FC = () => { ...@@ -380,17 +425,16 @@ export const Chat: React.FC = () => {
// workFlowSessionId 的传递逻辑: // workFlowSessionId 的传递逻辑:
// 1. 如果是重新问答(isReask === true),不传递 workFlowSessionId // 1. 如果是重新问答(isReask === true),不传递 workFlowSessionId
// 2. 如果是提质增效模式的自动调用(空问题),不传递 workFlowSessionId // 2. 如果是提质增效模式的自动调用(空问题),不传递 workFlowSessionId
// 3. 优先使用 extraParams 中的值 // 3. 优先使用 workflowExtraParams 中的值
// 4. 其次使用直接传递的参数 // 4. 其次使用直接传递的参数
// 5. 如果都没有,从会话历史中找到最后一个有 workFlowSessionId 的答案 // 5. 如果都没有,从会话历史中找到最后一个有 workFlowSessionId 的答案
const isReask = extraParams?.isReask === true const isReask = workflowExtraParams?.isReask === true
// 判断是否为提质增效自动调用:空问题且 toolId 为提质增效
const isAutoQualityImprovement = isAutoSubmitQualityImprovementRef.current const isAutoQualityImprovement = isAutoSubmitQualityImprovementRef.current
// 仅在指定工具下传递/回填 workFlowSessionId,避免其他工具被误带 // 已选择工具(非制度活化)时允许携带 / 回填 workFlowSessionId
const shouldUseWorkflowSession = resolvedToolId === '6712395743241' || resolvedToolId === '6712395743242' const shouldUseWorkflowSession = Boolean(resolvedToolId && String(resolvedToolId).trim() !== '')
if (shouldUseWorkflowSession && !isReask && !isAutoQualityImprovement) { if (shouldUseWorkflowSession && !isReask && !isAutoQualityImprovement) {
if (extraParams?.workFlowSessionId) { if (workflowExtraParams?.workFlowSessionId) {
requestBody.workFlowSessionId = extraParams.workFlowSessionId requestBody.workFlowSessionId = workflowExtraParams.workFlowSessionId
} }
else if (workFlowSessionId) { else if (workFlowSessionId) {
requestBody.workFlowSessionId = workFlowSessionId requestBody.workFlowSessionId = workFlowSessionId
...@@ -409,11 +453,11 @@ export const Chat: React.FC = () => { ...@@ -409,11 +453,11 @@ export const Chat: React.FC = () => {
} }
// 如果传入了 recordType,优先使用传入的值(workflow 提交场景) // 如果传入了 recordType,优先使用传入的值(workflow 提交场景)
if (extraParams?.recordType) { if (workflowExtraParams?.recordType) {
requestBody.recordType = extraParams.recordType requestBody.recordType = workflowExtraParams.recordType
// 如果同时传入了 busiType,也使用传入的值 // 如果同时传入了 busiType,也使用传入的值
if (extraParams?.busiType) { if (workflowExtraParams?.busiType) {
requestBody.busiType = extraParams.busiType requestBody.busiType = workflowExtraParams.busiType
} }
} }
// 制度活化:toolId 为空字符串或 undefined // 制度活化:toolId 为空字符串或 undefined
...@@ -422,21 +466,18 @@ export const Chat: React.FC = () => { ...@@ -422,21 +466,18 @@ export const Chat: React.FC = () => {
requestBody.busiType = '01' requestBody.busiType = '01'
requestBody.recordType = '01' requestBody.recordType = '01'
} }
// 提质增效:toolId 为 6712395743241
else if (resolvedToolId === '6712395743241') {
requestBody.busiType = '01'
requestBody.recordType = 'B03'
}
// 数据助手:toolId 为 6712395743240
else if (resolvedToolId === '6712395743240') {
requestBody.busiType = '01'
requestBody.recordType = '01'
}
// 兜底逻辑:如果 toolId 不在预期范围内,默认使用制度活化的参数
else { else {
requestBody.toolId = resolvedToolId const workflowPair = getWorkflowToolIdAndRecordTypeForSubmit()
requestBody.busiType = '01' if (workflowPair?.toolId && workflowPair?.recordType) {
requestBody.recordType = '01' requestBody.toolId = workflowPair.toolId
requestBody.busiType = workflowExtraParams?.busiType ?? extraParams?.busiType ?? '01'
requestBody.recordType = workflowPair.recordType
}
else {
requestBody.toolId = resolvedToolId
requestBody.busiType = '01'
requestBody.recordType = '01'
}
} }
fetchStreamResponse( fetchStreamResponse(
...@@ -538,6 +579,46 @@ export const Chat: React.FC = () => { ...@@ -538,6 +579,46 @@ export const Chat: React.FC = () => {
dispatch(fetchConversations()) dispatch(fetchConversations())
}, 2000) }, 2000)
} }
// 业务洞察工作流处理:检查 AI 返回的 answer 是否需要开启子工作流
const currentItems = allItems
let lastAiItem = null
for (let i = currentItems.length - 1; i >= 0; i--) {
if (currentItems[i].role === 'ai' && currentItems[i].answerList?.[0]) {
lastAiItem = currentItems[i]
break
}
}
if (lastAiItem) {
const lastAnswer = lastAiItem.answerList?.[0]
const wf = businessInsightWorkflowRef.current
const shouldHandleWorkflowAnswer = Boolean(lastAnswer?.workFlowSessionId)
|| Boolean(wf?.mainWorkflowSessionId)
|| Boolean(wf?.isInSubWorkflow)
if (lastAnswer && shouldHandleWorkflowAnswer) {
let parsedAnswer = null
try {
parsedAnswer = JSON.parse(lastAnswer.answer || '')
}
catch {
// 不是 JSON,保持正常回答
}
if (parsedAnswer && parsedAnswer.recordType && parsedAnswer.question) {
dispatch(setBusinessInsightSubWorkflow({
sessionId: '',
recordType: parsedAnswer.recordType,
}))
dispatch(setBusinessInsightPendingQuestion(parsedAnswer.question))
}
else if (businessInsightWorkflowRef.current.isInSubWorkflow) {
if (parsedAnswer && parsedAnswer.end === true) {
dispatch(exitBusinessInsightSubWorkflow())
}
}
else if (lastAnswer.workFlowSessionId && !businessInsightWorkflowRef.current.mainWorkflowSessionId) {
dispatch(setBusinessInsightMainWorkflow(lastAnswer.workFlowSessionId))
}
}
}
}) })
.catch(() => { .catch(() => {
// 流式请求出错时的处理 // 流式请求出错时的处理
......
import React from 'react'
import { Input } from '@heroui/react'
export interface WorkflowFieldOption {
id?: string
text?: string
type?: string
}
export interface WorkflowFieldItem {
key: string
label: string
type: string
required?: boolean
multiple?: boolean
options?: WorkflowFieldOption[]
}
interface CardWorkflowFieldProps {
field: WorkflowFieldItem
value: string | string[]
disabled: boolean
onChange: (key: string, value: string | string[]) => void
}
export const CardWorkflowField: React.FC<CardWorkflowFieldProps> = ({
field,
value,
disabled,
onChange,
}) => {
const requiredMark = field.required ? '*' : ''
const label = `${requiredMark}${field.label}`
if (field.type === 'select') {
if (field.multiple) {
const selectedValues = Array.isArray(value) ? value : []
return (
<div>
<label className="block text-[14px] text-[#4b5563] mb-[6px]">{label}</label>
<select
multiple
value={selectedValues}
disabled={disabled}
onChange={(e) => {
const nextValues = Array.from(e.target.selectedOptions).map(option => option.value)
onChange(field.key, nextValues)
}}
className="w-full min-h-[96px] border border-[#d4d4d8] rounded-[12px] px-[12px] py-[8px] text-[14px] bg-white"
>
{(field.options || []).map(option => (
<option key={option.id || option.text} value={option.id || ''}>
{option.text || option.id}
</option>
))}
</select>
</div>
)
}
const selectedValue = typeof value === 'string' ? value : ''
return (
<div>
<label className="block text-[14px] text-[#4b5563] mb-[6px]">{label}</label>
<select
value={selectedValue}
disabled={disabled}
onChange={e => onChange(field.key, e.target.value)}
className="w-full h-[40px] border border-[#d4d4d8] rounded-[12px] px-[12px] text-[14px] bg-white"
>
<option value="">
请选择
{field.label}
</option>
{(field.options || []).map(option => (
<option key={option.id || option.text} value={option.id || ''}>
{option.text || option.id}
</option>
))}
</select>
</div>
)
}
return (
<Input
label={label}
value={typeof value === 'string' ? value : ''}
onChange={e => onChange(field.key, e.target.value)}
isDisabled={disabled}
variant="bordered"
className="w-full"
/>
)
}
import React, { useEffect, useMemo, useRef, useState } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Button, Input } from '@heroui/react' import { Button } from '@heroui/react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { CardWorkflowField, type WorkflowFieldItem } from './CardWorkflowField'
import type { Answer, Attachment } from '@/types/chat' import type { Answer, Attachment } from '@/types/chat'
interface CardWorkflowFormProps { interface CardWorkflowFormProps {
...@@ -18,7 +19,7 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -18,7 +19,7 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
isSubmitting = false, isSubmitting = false,
isCompleted = false, isCompleted = false,
}) => { }) => {
const [formValues, setFormValues] = useState<Record<string, string>>({}) const [formValues, setFormValues] = useState<Record<string, string | string[]>>({})
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const hasAutoSubmittedRef = useRef(false) // 防止重复自动提交 const hasAutoSubmittedRef = useRef(false) // 防止重复自动提交
const lastFirstQaFlagRef = useRef<boolean | undefined>(undefined) // 记录上次的 firstQaFlag 值 const lastFirstQaFlagRef = useRef<boolean | undefined>(undefined) // 记录上次的 firstQaFlag 值
...@@ -29,31 +30,50 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -29,31 +30,50 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
onSubmitWorkflowRef.current = onSubmitWorkflow onSubmitWorkflowRef.current = onSubmitWorkflow
}, [onSubmitWorkflow]) }, [onSubmitWorkflow])
// 获取 text 类型的字段,使用 useMemo 稳定引用 // 获取 workflow 字段(支持 text/select 等),使用 useMemo 稳定引用
const textFields = useMemo(() => { const workflowFields = useMemo<WorkflowFieldItem[]>(() => {
return attachment.content?.fieldList?.filter(field => field.type === 'text') || [] return (attachment.content?.fieldList || []).filter(field => Boolean(field?.key) && Boolean(field?.type))
}, [attachment.content?.fieldList]) }, [attachment.content?.fieldList])
const isFieldValueFilled = (field: WorkflowFieldItem, value: string | string[] | undefined) => {
if (Array.isArray(value)) {
return value.length > 0
}
return !!value && value.trim() !== ''
}
const buildParamValue = (value: string | string[] | undefined) => {
if (Array.isArray(value)) {
return value.join(',')
}
return value || ''
}
// 初始化表单值,如果 firstQaFlag 为 true,从 URL query 中读取值 // 初始化表单值,如果 firstQaFlag 为 true,从 URL query 中读取值
useEffect(() => { useEffect(() => {
const initialValues: Record<string, string> = {} const initialValues: Record<string, string | string[]> = {}
if (answer.firstQaFlag === true) { if (answer.firstQaFlag === true) {
// 从 URL query 参数中读取值,匹配 fieldList 中的 key // 从 URL query 参数中读取值,匹配 fieldList 中的 key
textFields.forEach((field) => { workflowFields.forEach((field) => {
const queryValue = searchParams.get(field.key) const queryValue = searchParams.get(field.key)
initialValues[field.key] = queryValue || '' if (field.type === 'select' && field.multiple) {
initialValues[field.key] = queryValue ? queryValue.split(',').map(item => item.trim()).filter(Boolean) : []
}
else {
initialValues[field.key] = queryValue || ''
}
}) })
} }
else { else {
// 正常初始化,所有字段为空 // 正常初始化,所有字段为空
textFields.forEach((field) => { workflowFields.forEach((field) => {
initialValues[field.key] = '' initialValues[field.key] = field.type === 'select' && field.multiple ? [] : ''
}) })
} }
setFormValues(initialValues) setFormValues(initialValues)
}, [textFields, searchParams, answer.firstQaFlag]) }, [workflowFields, searchParams, answer.firstQaFlag])
// 单独处理 firstQaFlag 的变化,重置自动提交标志 // 单独处理 firstQaFlag 的变化,重置自动提交标志
useEffect(() => { useEffect(() => {
...@@ -61,7 +81,8 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -61,7 +81,8 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
if (answer.firstQaFlag !== true) { if (answer.firstQaFlag !== true) {
hasAutoSubmittedRef.current = false hasAutoSubmittedRef.current = false
lastFirstQaFlagRef.current = false // 重置记录的值,确保下次变为 true 时能正确触发 lastFirstQaFlagRef.current = false // 重置记录的值,确保下次变为 true 时能正确触发
} else { }
else {
// 如果 firstQaFlag 为 true,初始化 lastFirstQaFlagRef(如果还没有设置) // 如果 firstQaFlag 为 true,初始化 lastFirstQaFlagRef(如果还没有设置)
// 这样可以处理组件首次渲染时 firstQaFlag 已经是 true 的情况 // 这样可以处理组件首次渲染时 firstQaFlag 已经是 true 的情况
if (lastFirstQaFlagRef.current === undefined) { if (lastFirstQaFlagRef.current === undefined) {
...@@ -78,9 +99,9 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -78,9 +99,9 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
// 只在 firstQaFlag 首次变为 true 且尚未自动提交时执行 // 只在 firstQaFlag 首次变为 true 且尚未自动提交时执行
if (isFirstQaFlagChangedToTrue && !hasAutoSubmittedRef.current && !isSubmitting && !isCompleted) { if (isFirstQaFlagChangedToTrue && !hasAutoSubmittedRef.current && !isSubmitting && !isCompleted) {
// 检查是否有字段(textFields 不为空) // 检查是否有字段(workflowFields 不为空)
if (textFields.length === 0) { if (workflowFields.length === 0) {
console.log('[CardWorkflowForm] 自动提交:textFields 为空,跳过') console.log('[CardWorkflowForm] 自动提交:workflowFields 为空,跳过')
// 更新记录的值 // 更新记录的值
lastFirstQaFlagRef.current = answer.firstQaFlag lastFirstQaFlagRef.current = answer.firstQaFlag
return return
...@@ -88,10 +109,13 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -88,10 +109,13 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
// 检查是否所有必填字段都有值(从 URL query 中读取的) // 检查是否所有必填字段都有值(从 URL query 中读取的)
// 只校验 required === true 的字段 // 只校验 required === true 的字段
const requiredFields = textFields.filter(field => field.required === true) const requiredFields = workflowFields.filter(field => field.required === true)
const allRequiredFieldsHaveValues = requiredFields.length === 0 || requiredFields.every((field) => { const allRequiredFieldsHaveValues = requiredFields.length === 0 || requiredFields.every((field) => {
const queryValue = searchParams.get(field.key) const queryValue = searchParams.get(field.key)
return queryValue && queryValue.trim() !== '' if (field.type === 'select' && field.multiple) {
return !!queryValue && queryValue.split(',').map(item => item.trim()).filter(Boolean).length > 0
}
return !!queryValue && queryValue.trim() !== ''
}) })
console.log('[CardWorkflowForm] 自动提交检查:', { console.log('[CardWorkflowForm] 自动提交检查:', {
...@@ -99,11 +123,11 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -99,11 +123,11 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
hasAutoSubmitted: hasAutoSubmittedRef.current, hasAutoSubmitted: hasAutoSubmittedRef.current,
isSubmitting, isSubmitting,
isCompleted, isCompleted,
textFieldsCount: textFields.length, workflowFieldsCount: workflowFields.length,
requiredFieldsCount: requiredFields.length, requiredFieldsCount: requiredFields.length,
allRequiredFieldsHaveValues, allRequiredFieldsHaveValues,
requiredFields: requiredFields.map(f => ({ key: f.key, label: f.label })), requiredFields: requiredFields.map(f => ({ key: f.key, label: f.label })),
textFields: textFields.map(f => ({ key: f.key, label: f.label, required: f.required })), workflowFields: workflowFields.map(f => ({ key: f.key, label: f.label, required: f.required, type: f.type })),
}) })
if (allRequiredFieldsHaveValues) { if (allRequiredFieldsHaveValues) {
...@@ -111,12 +135,18 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -111,12 +135,18 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
hasAutoSubmittedRef.current = true hasAutoSubmittedRef.current = true
// 更新记录的值 // 更新记录的值
lastFirstQaFlagRef.current = answer.firstQaFlag lastFirstQaFlagRef.current = answer.firstQaFlag
// 构建 paramMap // 构建 paramMap
const paramMap: Record<string, string> = {} const paramMap: Record<string, string> = {}
textFields.forEach((field) => { workflowFields.forEach((field) => {
const queryValue = searchParams.get(field.key) const queryValue = searchParams.get(field.key)
paramMap[field.key] = queryValue || '' if (field.type === 'select' && field.multiple) {
const values = queryValue ? queryValue.split(',').map(item => item.trim()).filter(Boolean) : []
paramMap[field.key] = values.join(',')
}
else {
paramMap[field.key] = queryValue || ''
}
}) })
// 获取 recordId 和 workFlowSessionId(从 answer 中获取,第一次可能没有) // 获取 recordId 和 workFlowSessionId(从 answer 中获取,第一次可能没有)
...@@ -136,24 +166,25 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -136,24 +166,25 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
return () => { return () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
} }
} else { }
else {
console.log('[CardWorkflowForm] 自动提交:必填字段值不完整,跳过', { console.log('[CardWorkflowForm] 自动提交:必填字段值不完整,跳过', {
requiredFields: requiredFields.map(f => ({ key: f.key, label: f.label, value: searchParams.get(f.key) })), requiredFields: requiredFields.map(f => ({ key: f.key, label: f.label, value: searchParams.get(f.key) })),
allFields: textFields.map(f => ({ key: f.key, label: f.label, required: f.required, value: searchParams.get(f.key) })), allFields: workflowFields.map(f => ({ key: f.key, label: f.label, required: f.required, value: searchParams.get(f.key), type: f.type })),
}) })
// 更新记录的值 // 更新记录的值
lastFirstQaFlagRef.current = answer.firstQaFlag lastFirstQaFlagRef.current = answer.firstQaFlag
} }
} else { }
else {
// 更新记录的值(即使不执行自动提交,也要更新记录) // 更新记录的值(即使不执行自动提交,也要更新记录)
lastFirstQaFlagRef.current = answer.firstQaFlag lastFirstQaFlagRef.current = answer.firstQaFlag
} }
// 移除 onSubmitWorkflow 和 textFields 作为依赖项,使用 useRef 保存引用,避免函数引用变化或 textFields 更新导致重复触发 // 移除 onSubmitWorkflow 和 textFields 作为依赖项,使用 useRef 保存引用,避免函数引用变化或 textFields 更新导致重复触发
// textFields 更新不应该触发重新提交,因为我们已经检查了 firstQaFlag 的变化 // textFields 更新不应该触发重新提交,因为我们已经检查了 firstQaFlag 的变化
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answer.firstQaFlag, isSubmitting, isCompleted, searchParams]) }, [answer.firstQaFlag, isSubmitting, isCompleted, searchParams])
const handleInputChange = (key: string, value: string) => { const handleFieldChange = (key: string, value: string | string[]) => {
setFormValues(prev => ({ setFormValues(prev => ({
...prev, ...prev,
[key]: value, [key]: value,
...@@ -163,8 +194,8 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -163,8 +194,8 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
const handleSubmit = () => { const handleSubmit = () => {
// 构建 paramMap // 构建 paramMap
const paramMap: Record<string, string> = {} const paramMap: Record<string, string> = {}
textFields.forEach((field) => { workflowFields.forEach((field) => {
paramMap[field.key] = formValues[field.key] || '' paramMap[field.key] = buildParamValue(formValues[field.key])
}) })
// 获取 recordId 和 workFlowSessionId(从 answer 中获取,第一次可能没有) // 获取 recordId 和 workFlowSessionId(从 answer 中获取,第一次可能没有)
...@@ -174,8 +205,10 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -174,8 +205,10 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
onSubmitWorkflow(paramMap, recordId, workFlowSessionId) onSubmitWorkflow(paramMap, recordId, workFlowSessionId)
} }
// 检查是否有 text 类型的字段 const hasRequiredMissing = workflowFields.some(field => field.required && !isFieldValueFilled(field, formValues[field.key]))
if (textFields.length === 0) {
// 检查是否有可渲染字段
if (workflowFields.length === 0) {
return null return null
} }
...@@ -187,22 +220,20 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -187,22 +220,20 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
return ( return (
<div className="card-workflow-form"> <div className="card-workflow-form">
<div className="flex flex-col gap-[12px]"> <div className="flex flex-col gap-[12px]">
{textFields.map(field => ( {workflowFields.map(field => (
<div key={field.key}> <div key={field.key}>
<Input <CardWorkflowField
label={field.label} field={field}
value={formValues[field.key] || ''} value={formValues[field.key] ?? (field.type === 'select' && field.multiple ? [] : '')}
onChange={e => handleInputChange(field.key, e.target.value)} onChange={handleFieldChange}
isDisabled={isSubmitting || isCompleted || (answer.firstQaFlag === true && hasAutoSubmittedRef.current)} disabled={isSubmitting || isCompleted || (answer.firstQaFlag === true && hasAutoSubmittedRef.current)}
variant="bordered"
className="w-full"
/> />
</div> </div>
))} ))}
<Button <Button
color="primary" color="primary"
onPress={handleSubmit} onPress={handleSubmit}
isDisabled={isSubmitting || isCompleted || (answer.firstQaFlag === true && hasAutoSubmittedRef.current)} isDisabled={isSubmitting || isCompleted || hasRequiredMissing || (answer.firstQaFlag === true && hasAutoSubmittedRef.current)}
isLoading={isSubmitting} isLoading={isSubmitting}
className="mt-[8px]" className="mt-[8px]"
> >
......
...@@ -173,8 +173,8 @@ export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({ ...@@ -173,8 +173,8 @@ export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({
)} )}
{/* 工作流卡片:card-workflow */} {/* 工作流卡片:card-workflow */}
{attachment.type === 'card-workflow' {attachment.type === 'card-workflow'
&& attachment.content?.name === 'form_input' && attachment.content?.name === 'form_input'
&& attachment.content?.fieldList?.some(field => field.type === 'text') && ( && attachment.content?.fieldList?.length > 0 && (
<CardWorkflowForm <CardWorkflowForm
key={`workflow-form-${attachment.id || index}-${answer.recordId || 'new'}`} key={`workflow-form-${attachment.id || index}-${answer.recordId || 'new'}`}
attachment={attachment} attachment={attachment}
......
...@@ -7,7 +7,7 @@ import styles from './Home.module.less' ...@@ -7,7 +7,7 @@ import styles from './Home.module.less'
import { QuestionList } from './components/QuestionList' import { QuestionList } from './components/QuestionList'
import HomeIcon2 from '@/assets/homeIcon2.png' import HomeIcon2 from '@/assets/homeIcon2.png'
import SmartIce from '@/assets/smart-ice.png' import SmartIce from '@/assets/smart-ice.png'
import { clearCurrentToolId, fetchConversations, setCurrentConversation, setCurrentToolId, setNavigationFlag } from '@/store/conversationSlice' import { clearBusinessInsightWorkflow, clearCurrentToolId, fetchConversations, setCurrentConversation, setCurrentToolId, setNavigationFlag } from '@/store/conversationSlice'
import { useAppDispatch, useAppSelector } from '@/store/hook' import { useAppDispatch, useAppSelector } from '@/store/hook'
import { fetchEfficiencyQuestionList } from '@/api/home' import { fetchEfficiencyQuestionList } from '@/api/home'
import SdreamLoading from '@/components/SdreamLoading' import SdreamLoading from '@/components/SdreamLoading'
...@@ -297,6 +297,11 @@ export const Home: React.FC = () => { ...@@ -297,6 +297,11 @@ export const Home: React.FC = () => {
// 仅在点击“业务洞察”工具按钮时调用业务洞察常见问题接口;其余情况保持原有逻辑 // 仅在点击“业务洞察”工具按钮时调用业务洞察常见问题接口;其余情况保持原有逻辑
if (isBusinessInsightClick) { if (isBusinessInsightClick) {
setIsBusinessInsightMode(true) setIsBusinessInsightMode(true)
// 清空业务洞察工作流状态
dispatch(clearBusinessInsightWorkflow())
// 保存业务洞察工具ID
dispatch(setCurrentToolId('6712395743242'))
safeSessionStorageSetItem('currentToolId', '6712395743242')
// 先清空旧数据 // 先清空旧数据
setOtherQuestions((prev: any) => ({ setOtherQuestions((prev: any) => ({
...prev, ...prev,
......
...@@ -13,6 +13,14 @@ const initialState: ConversationState = { ...@@ -13,6 +13,14 @@ const initialState: ConversationState = {
shouldNavigateToNewConversation: false, shouldNavigateToNewConversation: false,
shouldSendQuestion: '', shouldSendQuestion: '',
currentToolId: undefined, currentToolId: undefined,
businessInsightWorkflow: {
mainWorkflowSessionId: null,
subWorkflowSessionId: null,
subWorkflowRecordType: null,
paramMap: {},
isInSubWorkflow: false,
pendingQuestion: null,
},
} }
export const fetchConversations = createAsyncThunk( export const fetchConversations = createAsyncThunk(
...@@ -119,6 +127,47 @@ const conversationSlice = createSlice({ ...@@ -119,6 +127,47 @@ const conversationSlice = createSlice({
clearCurrentToolId: (state) => { clearCurrentToolId: (state) => {
state.currentToolId = undefined state.currentToolId = undefined
}, },
setBusinessInsightMainWorkflow: (state, action: PayloadAction<string>) => {
state.businessInsightWorkflow.mainWorkflowSessionId = action.payload
state.businessInsightWorkflow.isInSubWorkflow = false
},
setBusinessInsightSubWorkflow: (state, action: PayloadAction<{ sessionId: string, recordType: string }>) => {
state.businessInsightWorkflow.subWorkflowSessionId = action.payload.sessionId
state.businessInsightWorkflow.subWorkflowRecordType = action.payload.recordType
state.businessInsightWorkflow.isInSubWorkflow = true
state.businessInsightWorkflow.paramMap = {}
},
updateBusinessInsightParamMap: (state, action: PayloadAction<Record<string, string>>) => {
state.businessInsightWorkflow.paramMap = {
...state.businessInsightWorkflow.paramMap,
...action.payload,
}
},
clearBusinessInsightSubWorkflow: (state) => {
state.businessInsightWorkflow.subWorkflowSessionId = null
state.businessInsightWorkflow.subWorkflowRecordType = null
state.businessInsightWorkflow.isInSubWorkflow = false
state.businessInsightWorkflow.paramMap = {}
},
exitBusinessInsightSubWorkflow: (state) => {
state.businessInsightWorkflow.isInSubWorkflow = false
state.businessInsightWorkflow.subWorkflowSessionId = null
state.businessInsightWorkflow.subWorkflowRecordType = null
state.businessInsightWorkflow.paramMap = {}
},
setBusinessInsightPendingQuestion: (state, action: PayloadAction<string | null>) => {
state.businessInsightWorkflow.pendingQuestion = action.payload
},
clearBusinessInsightWorkflow: (state) => {
state.businessInsightWorkflow = {
mainWorkflowSessionId: null,
subWorkflowSessionId: null,
subWorkflowRecordType: null,
paramMap: {},
isInSubWorkflow: false,
pendingQuestion: null,
}
},
removeConversation: (state, action: PayloadAction<string>) => { removeConversation: (state, action: PayloadAction<string>) => {
state.conversations = state.conversations.filter(conv => conv.conversationId !== action.payload) state.conversations = state.conversations.filter(conv => conv.conversationId !== action.payload)
if (state.currentConversationId === action.payload) { if (state.currentConversationId === action.payload) {
...@@ -164,6 +213,6 @@ const conversationSlice = createSlice({ ...@@ -164,6 +213,6 @@ const conversationSlice = createSlice({
}, },
}) })
export const { setCurrentConversation, clearCurrentConversation, clearNavigationFlag, setNavigationFlag, clearShouldSendQuestion, setShouldSendQuestion, setCurrentToolId, clearCurrentToolId } = conversationSlice.actions export const { setCurrentConversation, clearCurrentConversation, clearNavigationFlag, setNavigationFlag, clearShouldSendQuestion, setShouldSendQuestion, setCurrentToolId, clearCurrentToolId, setBusinessInsightMainWorkflow, setBusinessInsightSubWorkflow, updateBusinessInsightParamMap, clearBusinessInsightSubWorkflow, exitBusinessInsightSubWorkflow, setBusinessInsightPendingQuestion, clearBusinessInsightWorkflow } = conversationSlice.actions
export default conversationSlice.reducer export default conversationSlice.reducer
...@@ -21,6 +21,13 @@ interface AttachmentContentField { ...@@ -21,6 +21,13 @@ interface AttachmentContentField {
label: string label: string
type: string type: string
required?: boolean required?: boolean
multiple?: boolean
value?: string
options?: {
id?: string
text?: string
type?: string
}[]
} }
interface AttachmentContent { interface AttachmentContent {
......
...@@ -16,4 +16,12 @@ export interface ConversationState { ...@@ -16,4 +16,12 @@ export interface ConversationState {
error: string | null error: string | null
shouldSendQuestion: string shouldSendQuestion: string
currentToolId: string | undefined currentToolId: string | undefined
businessInsightWorkflow: {
mainWorkflowSessionId: string | null
subWorkflowSessionId: string | null
subWorkflowRecordType: string | null
paramMap: Record<string, string>
isInSubWorkflow: boolean
pendingQuestion: string | null
}
} }
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