Commit 8204814b by weiyudumei

feat: 实现 card-workflow 工作流卡片功能

- 新增 CardWorkflowForm 组件处理表单输入
- 支持根据 fieldList 动态渲染 text 类型输入框
- 实现 paramMap 和 recordId 参数传递
- 完成后禁用卡片内输入框和提交按钮
- 页面最下方 ChatEditor 不受影响
parent 410bdbde
...@@ -27,3 +27,43 @@ Preview the production build locally: ...@@ -27,3 +27,43 @@ Preview the production build locally:
```bash ```bash
pnpm preview pnpm preview
``` ```
dialog_input 一个text
form_input 多个text
recordId
示例:当前返回的cardList为
[
{
content: {
name: 'form_input',
fieldList: [
{
key: 'text_input',
label: '策略id',
type: 'text'
},
{
key: 'text_input2',
label: '版本号',
type: 'text'
}
]
},
type: 'card-workflow'
}
]
第一步判断 type 是 card-workflow,第二步判断name是form_input,第三部判断fieldList里面的item的type有text则展示此卡片,卡片为fieldList里面有几个text就显示几个输入框,对应显示label,下面显示一个提交按钮同时禁用最下面的输入框,当用户提交成功之后调用流式接口返回全部完成,禁用这个按钮,最下面的按钮放开,调用流式接口的时候追加参数
{
recordId: 上一次流式接口返回的recordId,
paramMap: {
key为fieldList item里面的key: value: 用户输入的值,
}
}
注意,只有这个卡片调用流式接口的时候才传paramMap字段,recordId第一次没有的时候不传,从第二次调用开始传上一次返回的recordId,这个recordId和busiType同级,你按照这个来实现有不清楚的随时问我
...@@ -192,7 +192,12 @@ export const Chat: React.FC = () => { ...@@ -192,7 +192,12 @@ export const Chat: React.FC = () => {
} }
/** 提交问题 */ /** 提交问题 */
const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string) => { const handleSubmitQuestion = async (
question: string,
productCode?: string,
toolId?: string,
extraParams?: { paramMap?: Record<string, string>, recordId?: string },
) => {
// 优先读取缓存中的 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
...@@ -246,6 +251,14 @@ export const Chat: React.FC = () => { ...@@ -246,6 +251,14 @@ export const Chat: React.FC = () => {
toolId: resolvedToolId ?? '', toolId: resolvedToolId ?? '',
} }
// 如果是 workflow 提交,添加 paramMap 和 recordId
if (extraParams?.paramMap) {
requestBody.paramMap = extraParams.paramMap
}
if (extraParams?.recordId) {
requestBody.recordId = extraParams.recordId
}
// 制度活化:toolId 为空字符串或 undefined // 制度活化:toolId 为空字符串或 undefined
if (!resolvedToolId || resolvedToolId === '') { if (!resolvedToolId || resolvedToolId === '') {
requestBody.toolId = '' requestBody.toolId = ''
...@@ -378,6 +391,15 @@ export const Chat: React.FC = () => { ...@@ -378,6 +391,15 @@ export const Chat: React.FC = () => {
}) })
} }
/** 处理 workflow 表单提交 */
const handleWorkflowSubmit = async (paramMap: Record<string, string>, recordId?: string) => {
// workflow 提交时,question 可以为空字符串,但需要传递 paramMap 和 recordId
await handleSubmitQuestion('', undefined, undefined, {
paramMap,
recordId,
})
}
/** 获取qa记录 */ /** 获取qa记录 */
const getUserQaRecordPage = useCallback(async (conversationId: string, toolId?: string) => { const getUserQaRecordPage = useCallback(async (conversationId: string, toolId?: string) => {
// 检查是否正在处理相同的 conversationId,避免重复调用 // 检查是否正在处理相同的 conversationId,避免重复调用
...@@ -897,6 +919,8 @@ export const Chat: React.FC = () => { ...@@ -897,6 +919,8 @@ export const Chat: React.FC = () => {
showIndex={0} showIndex={0}
record={record} record={record}
index={index} index={index}
onWorkflowSubmit={handleWorkflowSubmit}
isWorkflowSubmitting={isAsking}
/> />
)} )}
</div> </div>
......
import React, { useEffect, useState } from 'react'
import { Button, Input } from '@heroui/react'
import type { Answer, Attachment } from '@/types/chat'
interface CardWorkflowFormProps {
attachment: Attachment
answer: Answer
onSubmitWorkflow: (paramMap: Record<string, string>, recordId?: string) => void
isSubmitting?: boolean
isCompleted?: boolean
}
export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
attachment,
answer,
onSubmitWorkflow,
isSubmitting = false,
isCompleted = false,
}) => {
const [formValues, setFormValues] = useState<Record<string, string>>({})
// 获取 text 类型的字段
const textFields = attachment.content?.fieldList?.filter(field => field.type === 'text') || []
// 初始化表单值
useEffect(() => {
const initialValues: Record<string, string> = {}
textFields.forEach((field) => {
initialValues[field.key] = ''
})
setFormValues(initialValues)
}, [attachment])
const handleInputChange = (key: string, value: string) => {
setFormValues(prev => ({
...prev,
[key]: value,
}))
}
const handleSubmit = () => {
// 构建 paramMap
const paramMap: Record<string, string> = {}
textFields.forEach((field) => {
paramMap[field.key] = formValues[field.key] || ''
})
// 获取 recordId(从 answer 中获取,第一次可能没有)
const recordId = answer.recordId
onSubmitWorkflow(paramMap, recordId)
}
// 检查是否有 text 类型的字段
if (textFields.length === 0) {
return null
}
return (
<div className="card-workflow-form">
<div className="flex flex-col gap-[12px]">
{textFields.map(field => (
<div key={field.key}>
<Input
label={field.label}
value={formValues[field.key] || ''}
onChange={e => handleInputChange(field.key, e.target.value)}
isDisabled={isSubmitting || isCompleted}
variant="bordered"
className="w-full"
/>
</div>
))}
<Button
color="primary"
onPress={handleSubmit}
isDisabled={isSubmitting || isCompleted}
isLoading={isSubmitting}
className="mt-[8px]"
>
{isCompleted ? '已完成' : '提交'}
</Button>
</div>
</div>
)
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { Button, Link } from '@heroui/react' import { Button, Link } from '@heroui/react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { useState } from 'react' import { useState } from 'react'
import { CardWorkflowForm } from './CardWorkflowForm'
import type { Answer, Attachment } from '@/types/chat' import type { Answer, Attachment } from '@/types/chat'
import AnswerProDetailIcon from '@/assets/svg/answerProDetail.svg?react' import AnswerProDetailIcon from '@/assets/svg/answerProDetail.svg?react'
import CardNavImg from '@/assets/card-nav.png' import CardNavImg from '@/assets/card-nav.png'
...@@ -16,12 +17,16 @@ interface ChatAnswerAttachmentProps { ...@@ -16,12 +17,16 @@ interface ChatAnswerAttachmentProps {
answer: Answer answer: Answer
isLastAnswer?: boolean isLastAnswer?: boolean
onSubmitQuestion?: (question: string, productCode?: string) => void onSubmitQuestion?: (question: string, productCode?: string) => void
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
isWorkflowSubmitting?: boolean
} }
export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({ export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({
answer, answer,
isLastAnswer, isLastAnswer,
onSubmitQuestion, onSubmitQuestion,
onWorkflowSubmit,
isWorkflowSubmitting = false,
}) => { }) => {
const [previewModalOpen, setPreviewModalOpen] = useState(false) const [previewModalOpen, setPreviewModalOpen] = useState(false)
const [currentDoc, setCurrentDoc] = useState<any>(null) const [currentDoc, setCurrentDoc] = useState<any>(null)
...@@ -161,7 +166,20 @@ export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({ ...@@ -161,7 +166,20 @@ export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({
</ul> </ul>
</div> </div>
)} )}
{attachment.type?.includes('card-') && ( {/* 工作流卡片:card-workflow */}
{attachment.type === 'card-workflow'
&& attachment.content?.name === 'form_input'
&& attachment.content?.fieldList?.some(field => field.type === 'text') && (
<CardWorkflowForm
attachment={attachment}
answer={answer}
onSubmitWorkflow={onWorkflowSubmit || (() => {})}
isSubmitting={isWorkflowSubmitting}
isCompleted={answer.endAnswerFlag}
/>
)}
{/* 其他 card-* 类型 */}
{attachment.type?.includes('card-') && attachment.type !== 'card-workflow' && (
<div onClick={() => handleClickCard(attachment)}> <div onClick={() => handleClickCard(attachment)}>
{attachment.type === 'card-nav' && ( {attachment.type === 'card-nav' && (
<img className="w-full max-w-[400px] cursor-pointer" src={CardNavImg} alt="" /> <img className="w-full max-w-[400px] cursor-pointer" src={CardNavImg} alt="" />
......
...@@ -18,9 +18,11 @@ interface ChatAnswerBoxProps { ...@@ -18,9 +18,11 @@ interface ChatAnswerBoxProps {
index: number index: number
onSubmitQuestion: (question: string, productCode?: string) => void onSubmitQuestion: (question: string, productCode?: string) => void
onRecommendLoadingChange?: (loading: boolean) => void onRecommendLoadingChange?: (loading: boolean) => void
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
isWorkflowSubmitting?: boolean
} }
export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, isLastAnswer, onSubmitQuestion, onRecommendLoadingChange }) => { export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, isLastAnswer, onSubmitQuestion, onRecommendLoadingChange, onWorkflowSubmit, isWorkflowSubmitting }) => {
const [isShowRecommend, setIsShowRecommend] = useState(false) const [isShowRecommend, setIsShowRecommend] = useState(false)
const [recommendUseAnswer, setRecommendUseAnswer] = useState<Answer>() const [recommendUseAnswer, setRecommendUseAnswer] = useState<Answer>()
const [innerRecord, setInnerRecord] = useState<ChatRecord>(record) const [innerRecord, setInnerRecord] = useState<ChatRecord>(record)
...@@ -75,6 +77,8 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, ...@@ -75,6 +77,8 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex,
isLastAnswer={isLastAnswer} isLastAnswer={isLastAnswer}
answer={item} answer={item}
hideOperate={shouldHideOperate} hideOperate={shouldHideOperate}
onWorkflowSubmit={onWorkflowSubmit}
isWorkflowSubmitting={isWorkflowSubmitting}
/> />
)} )}
{!item.isShow && !item.isChatMaxCount && ( {!item.isShow && !item.isChatMaxCount && (
...@@ -86,6 +90,8 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, ...@@ -86,6 +90,8 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex,
onComplate={() => handleComplate(item)} onComplate={() => handleComplate(item)}
answer={item} answer={item}
hideOperate={shouldHideOperate} hideOperate={shouldHideOperate}
onWorkflowSubmit={onWorkflowSubmit}
isWorkflowSubmitting={isWorkflowSubmitting}
/> />
)} )}
{!item.isShow && item.isChatMaxCount && <ChatMaxCount />} {!item.isShow && item.isChatMaxCount && <ChatMaxCount />}
......
...@@ -17,6 +17,8 @@ interface ChatAnswerParserProps { ...@@ -17,6 +17,8 @@ interface ChatAnswerParserProps {
onSubmitQuestion: (question: string, productCode?: string) => void onSubmitQuestion: (question: string, productCode?: string) => void
/** 是否在当前回答中隐藏点赞/点踩/复制按钮(例如策略页自动分析、重新分析) */ /** 是否在当前回答中隐藏点赞/点踩/复制按钮(例如策略页自动分析、重新分析) */
hideOperate?: boolean hideOperate?: boolean
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
isWorkflowSubmitting?: boolean
} }
function CheckIcon({ ...props }) { function CheckIcon({ ...props }) {
...@@ -37,7 +39,7 @@ function CheckIcon({ ...props }) { ...@@ -37,7 +39,7 @@ function CheckIcon({ ...props }) {
) )
} }
export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer, onTyping, onComplate, answer, isStopTyping, onSubmitQuestion, hideOperate }) => { export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer, onTyping, onComplate, answer, isStopTyping, onSubmitQuestion, hideOperate, onWorkflowSubmit, isWorkflowSubmitting }) => {
const formatAnswer = formatMarkdown(answer.answer || '') const formatAnswer = formatMarkdown(answer.answer || '')
const [displayedText, setDisplayedText] = useState('') const [displayedText, setDisplayedText] = useState('')
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
...@@ -100,7 +102,7 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer ...@@ -100,7 +102,7 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer
} }
if (answer.cardList && answer.cardList.length > 0) { if (answer.cardList && answer.cardList.length > 0) {
const cardsWithUrl = answer.cardList[0].url || '' const cardsWithUrl = answer.cardList[0].url || ''
// eslint-disable-next-line no-console
console.log('准备跳转的URL:', cardsWithUrl) console.log('准备跳转的URL:', cardsWithUrl)
if (cardsWithUrl) { if (cardsWithUrl) {
window.open(cardsWithUrl, '_blank') window.open(cardsWithUrl, '_blank')
...@@ -208,6 +210,8 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer ...@@ -208,6 +210,8 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer
isLastAnswer={isLastAnswer} isLastAnswer={isLastAnswer}
onSubmitQuestion={onSubmitQuestion} onSubmitQuestion={onSubmitQuestion}
answer={answer} answer={answer}
onWorkflowSubmit={onWorkflowSubmit}
isWorkflowSubmitting={isWorkflowSubmitting}
/> />
)} )}
......
...@@ -10,9 +10,11 @@ interface ChatAnswerShowerProps { ...@@ -10,9 +10,11 @@ interface ChatAnswerShowerProps {
isLastAnswer: boolean isLastAnswer: boolean
onSubmitQuestion: (question: string) => void onSubmitQuestion: (question: string) => void
hideOperate?: boolean hideOperate?: boolean
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
isWorkflowSubmitting?: boolean
} }
export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLastAnswer, onSubmitQuestion, hideOperate: hideOperateProp }) => { export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLastAnswer, onSubmitQuestion, hideOperate: hideOperateProp, onWorkflowSubmit, isWorkflowSubmitting }) => {
const hideOperateByCard = (answer.cardList || []).some(attachment => attachment.type === 'box' || attachment?.type?.includes('card-')) const hideOperateByCard = (answer.cardList || []).some(attachment => attachment.type === 'box' || attachment?.type?.includes('card-'))
const hideOperate = hideOperateByCard || hideOperateProp const hideOperate = hideOperateByCard || hideOperateProp
return ( return (
...@@ -25,7 +27,13 @@ export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLa ...@@ -25,7 +27,13 @@ export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLa
</div> </div>
)} )}
{answer.cardList && answer.cardList?.length !== 0 && ( {answer.cardList && answer.cardList?.length !== 0 && (
<ChatAnswerAttachment onSubmitQuestion={onSubmitQuestion} isLastAnswer={isLastAnswer} answer={answer} /> <ChatAnswerAttachment
onSubmitQuestion={onSubmitQuestion}
isLastAnswer={isLastAnswer}
answer={answer}
onWorkflowSubmit={onWorkflowSubmit}
isWorkflowSubmitting={isWorkflowSubmitting}
/>
)} )}
{/* 操作区:仅根据卡片类型 / 上层显式 hideOperate 控制是否展示 */} {/* 操作区:仅根据卡片类型 / 上层显式 hideOperate 控制是否展示 */}
{!hideOperate && <ChatAnswerOperate answer={answer} />} {!hideOperate && <ChatAnswerOperate answer={answer} />}
......
...@@ -337,7 +337,7 @@ export const TacticsChat: React.FC = () => { ...@@ -337,7 +337,7 @@ export const TacticsChat: React.FC = () => {
question: string | undefined, question: string | undefined,
productCode?: string, productCode?: string,
toolId?: string, toolId?: string,
extra?: { busiType?: string, recordType?: string, numberType?: string, includeQuestion?: boolean, includeTacticsMeta?: boolean, includeUserMeta?: boolean, includeOrderMeta?: boolean, isReanalyze?: boolean }, extra?: { busiType?: string, recordType?: string, numberType?: string, includeQuestion?: boolean, includeTacticsMeta?: boolean, includeUserMeta?: boolean, includeOrderMeta?: boolean, isReanalyze?: boolean, paramMap?: Record<string, string>, recordId?: string },
) => { ) => {
// 优先读取缓存中的 toolId,再回退到传参 // 优先读取缓存中的 toolId,再回退到传参
const sessionToolId = sessionStorage.getItem('currentToolId') ?? undefined const sessionToolId = sessionStorage.getItem('currentToolId') ?? undefined
...@@ -484,6 +484,13 @@ export const TacticsChat: React.FC = () => { ...@@ -484,6 +484,13 @@ export const TacticsChat: React.FC = () => {
if (shouldSendQuestion || (question && extra?.includeQuestion === false)) { if (shouldSendQuestion || (question && extra?.includeQuestion === false)) {
requestBody.question = question ?? '' requestBody.question = question ?? ''
} }
// 如果是 workflow 提交,添加 paramMap 和 recordId
if (extra?.paramMap) {
requestBody.paramMap = extra.paramMap
}
if (extra?.recordId) {
requestBody.recordId = extra.recordId
}
// 调试日志:检查最终请求参数 // 调试日志:检查最终请求参数
console.log('[TacticsChat] handleSubmitQuestion: final requestBody', { console.log('[TacticsChat] handleSubmitQuestion: final requestBody', {
workOrderIds: requestBody.workOrderIds, workOrderIds: requestBody.workOrderIds,
...@@ -605,6 +612,16 @@ export const TacticsChat: React.FC = () => { ...@@ -605,6 +612,16 @@ export const TacticsChat: React.FC = () => {
}) })
} }
/** 处理 workflow 表单提交 */
const handleWorkflowSubmit = async (paramMap: Record<string, string>, recordId?: string) => {
// workflow 提交时,question 可以为空字符串,但需要传递 paramMap 和 recordId
await handleSubmitQuestion(undefined, undefined, undefined, {
includeQuestion: false,
paramMap,
recordId,
})
}
/** 获取qa记录 */ /** 获取qa记录 */
const getUserQaRecordPage = useCallback(async (conversationId: string) => { const getUserQaRecordPage = useCallback(async (conversationId: string) => {
setIsLoading(true) setIsLoading(true)
...@@ -1164,6 +1181,8 @@ export const TacticsChat: React.FC = () => { ...@@ -1164,6 +1181,8 @@ export const TacticsChat: React.FC = () => {
index={index} index={index}
// 仅对最后一条 AI 回答的推荐问题 loading 状态进行跟踪 // 仅对最后一条 AI 回答的推荐问题 loading 状态进行跟踪
onRecommendLoadingChange={index === allItems.length - 1 ? setIsRecommendLoading : undefined} onRecommendLoadingChange={index === allItems.length - 1 ? setIsRecommendLoading : undefined}
onWorkflowSubmit={handleWorkflowSubmit}
isWorkflowSubmitting={isAsking}
/> />
)} )}
</div> </div>
......
...@@ -16,6 +16,12 @@ export interface AttachmentContentEmailItem { ...@@ -16,6 +16,12 @@ export interface AttachmentContentEmailItem {
emailAddr: string emailAddr: string
} }
interface AttachmentContentField {
key: string
label: string
type: string
}
interface AttachmentContent { interface AttachmentContent {
docList: AttachmentContentDoc[] docList: AttachmentContentDoc[]
description: string description: string
...@@ -26,6 +32,9 @@ interface AttachmentContent { ...@@ -26,6 +32,9 @@ interface AttachmentContent {
mailMsg?: string mailMsg?: string
receiver?: string receiver?: string
emailList?: AttachmentContentEmailItem[] emailList?: AttachmentContentEmailItem[]
// Workflow相关字段
name?: string
fieldList?: AttachmentContentField[]
} }
export interface Attachment { export interface Attachment {
......
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