Commit c877b1a4 by Liu

fix:底部按钮基本逻辑

parent e879c818
......@@ -60,3 +60,12 @@ export function fetchDownloadFile(docId: string) {
responseType: 'blob',
})
}
/**
* 工作流表单文件上传(知识库上传,返回文件访问 URL,写入 paramMap[field.key] 数组)
*/
export function fetchUploadWorkflowFile(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.post<{ data?: unknown }>('/api/v1/knowledge/upload', formData)
}
......@@ -5,7 +5,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useClickAway, useSessionStorageState } from 'ahooks'
import styles from './Navbar.module.less'
import { NavBarItem } from './components/NavBarItem'
import { clearNavigationFlag, createConversation } from '@/store/conversationSlice'
import { clearBusinessInsightWorkflow, clearNavigationFlag, createConversation } from '@/store/conversationSlice'
import { createTacticsConversation } from '@/store/tacticsSlice'
import type { WithAuthProps } from '@/auth/withAuth'
import { withAuth } from '@/auth/withAuth'
......@@ -15,6 +15,7 @@ import { isMobile } from '@/utils'
import { safeLocalStorageSetItem, waitForToken } from '@/lib/utils'
import { fetchUserQaRecordPage } from '@/api/conversation'
import { findToolRowBySessionToolId, getToolQuestionPairFromRow, readToolListFromSessionStorage, toolRowTriggersEmptyQuestionBootstrap } from '@/lib/toolRoleRecordTypes'
import { hasQaWorkflowContext } from '@/pages/Chat/helper'
// import { User } from './components/User'
interface NavbarProps {
......@@ -32,6 +33,7 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
const handleCreateConversation = () => {
const sessionToolId = sessionStorage.getItem('currentToolId') || undefined
dispatch(clearBusinessInsightWorkflow())
dispatch(createConversation({
conversationData: {
...(sessionToolId ? { toolId: sessionToolId } : {}),
......@@ -150,14 +152,19 @@ const NavbarBase: React.FC<NavbarProps & WithAuthProps> = ({ isHistoryVisible, c
await waitForToken()
const qaRes = await fetchUserQaRecordPage(currentConversationId, currentToolId)
const qaRecords = qaRes?.data || []
window.dispatchEvent(new CustomEvent('toolHistoryLoaded', {
detail: {
conversationId: currentConversationId,
toolId: currentToolId,
qaRecords: qaRes?.data || [],
qaRecords,
},
}))
// 最后一条已有 workflow 上下文则不再自动补问;否则静默首包一次
if (hasQaWorkflowContext(qaRecords))
return
const pair = getToolQuestionPairFromRow(row)
const recordTypeForAuto = pair.recordType || '01'
requestAnimationFrame(() => {
......
......@@ -49,11 +49,16 @@ export function findGeneralToolRow(toolList: unknown): ToolListRow | null {
return hit ?? null
}
export function getFirstToolListRow(toolList: unknown): ToolListRow | null {
const list = asToolRows(toolList)
return list.length > 0 ? list[0] : null
}
export function findToolRowBySessionToolId(toolList: unknown, sessionToolId: string | null | undefined): ToolListRow | null {
const list = asToolRows(toolList)
const tid = normalizeToolId(sessionToolId)
if (!tid)
return findGeneralToolRow(toolList)
return getFirstToolListRow(toolList)
return list.find(r => normalizeToolId(r.toolId) === tid) ?? null
}
......@@ -71,6 +76,19 @@ export function getToolQuestionPairFromRow(row: ToolListRow | null | undefined):
return { toolId, recordType }
}
/**
* 提问接口统一解析 toolId / recordType:按当前会话工具从 get_tool_list 取行,
* recordType 为 userRole 第一段(提问流),无则回退行上 recordType;无 toolId 时取制度活化/通用行。
*/
export function resolveSubmitQuestionToolPair(
sessionToolId?: string | null,
toolList?: ToolListRow[],
): { toolId: string, recordType: string } {
const list = toolList ?? readToolListFromSessionStorage()
const row = findToolRowBySessionToolId(list, sessionToolId)
return getToolQuestionPairFromRow(row)
}
/** 左侧常见问题 recordType */
export function getCommonQuestionRecordTypeForToolRow(row: ToolListRow | null | undefined): string {
if (!row)
......
import { getToolQuestionPairFromRow } from '@/lib/toolRoleRecordTypes'
import { getToolQuestionPairFromRow, resolveSubmitQuestionToolPair } from '@/lib/toolRoleRecordTypes'
import type { ToolListRow } from '@/types/toolList'
/**
......@@ -104,16 +104,15 @@ export function getToolListFirstPair(toolList: unknown[]): { toolId: string, rec
* 若已有流式返回缓存则优先;否则使用列表第 0 项的 userRole 首段(首次)。
*/
export function getWorkflowToolIdAndRecordTypeForRequest(toolList: unknown[]): { toolId: string, recordType: string } {
const last = getWorkflowLastToolPair()
if (last)
return last
const seed = getWorkflowRequestSeedPair()
if (seed)
return seed
const sessionToolId = typeof window !== 'undefined' ? sessionStorage.getItem('currentToolId') : null
const fromList = resolveSubmitQuestionToolPair(sessionToolId, Array.isArray(toolList) ? (toolList as import('@/types/toolList').ToolListRow[]) : undefined)
if (fromList.recordType)
return fromList
return getToolListFirstPair(toolList)
}
/** Chat 提交:流式最后一次优先,否则为本次点击写入的 seed(get_tool_list[0]) */
export function getWorkflowToolIdAndRecordTypeForSubmit(): { toolId: string, recordType: string } | null {
return getWorkflowLastToolPair() ?? getWorkflowRequestSeedPair()
/** Chat 提交:与 resolveSubmitQuestionToolPair 一致(get_tool_list 当前项 userRole 第一段) */
export function getWorkflowToolIdAndRecordTypeForSubmit(sessionToolId?: string | null): { toolId: string, recordType: string } {
const fromSession = typeof window !== 'undefined' ? sessionStorage.getItem('currentToolId') : null
return resolveSubmitQuestionToolPair(sessionToolId ?? fromSession ?? undefined)
}
import React from 'react'
import { Input } from '@heroui/react'
import {
WORKFLOW_FIELD_TYPE,
type WorkflowFieldOption,
type WorkflowFormField,
} from '../../lib/workflowCardForm'
import { CardWorkflowFileField } from './CardWorkflowFileField'
export interface WorkflowFieldOption {
id?: string
text?: string
type?: string
}
export interface WorkflowFieldItem {
key: string
label: string
type: string
required?: boolean
multiple?: boolean
options?: WorkflowFieldOption[]
}
export type { WorkflowFieldOption, WorkflowFormField as WorkflowFieldItem }
interface CardWorkflowFieldProps {
field: WorkflowFieldItem
field: WorkflowFormField
value: string | string[]
disabled: boolean
isUploading?: boolean
showRequiredError?: boolean
onChange: (key: string, value: string | string[]) => void
onPickFile?: (key: string, file: File) => void | Promise<void>
}
export const CardWorkflowField: React.FC<CardWorkflowFieldProps> = ({
field,
value,
disabled,
showRequiredError = false,
isUploading = false,
onChange,
onPickFile,
}) => {
const requiredMark = field.required ? '*' : ''
const label = `${requiredMark}${field.label}`
if (field.type === 'select') {
if (field.type === WORKFLOW_FIELD_TYPE.FILE) {
const fileName = typeof value === 'string' ? value : ''
return (
<CardWorkflowFileField
field={field}
fileName={fileName}
disabled={disabled}
isUploading={isUploading}
showRequiredError={showRequiredError}
onPickFile={(file) => {
void onPickFile?.(field.key, file)
}}
/>
)
}
if (field.type === WORKFLOW_FIELD_TYPE.SELECT) {
if (field.multiple) {
const selectedValues = Array.isArray(value) ? value : []
return (
......@@ -54,6 +69,12 @@ export const CardWorkflowField: React.FC<CardWorkflowFieldProps> = ({
</option>
))}
</select>
{showRequiredError && (
<p className="text-[12px] text-[#f31260] mt-[6px]">
请选择
{field.label}
</p>
)}
</div>
)
}
......@@ -78,11 +99,18 @@ export const CardWorkflowField: React.FC<CardWorkflowFieldProps> = ({
</option>
))}
</select>
{showRequiredError && (
<p className="text-[12px] text-[#f31260] mt-[6px]">
请选择
{field.label}
</p>
)}
</div>
)
}
return (
<div>
<Input
label={label}
value={typeof value === 'string' ? value : ''}
......@@ -90,6 +118,14 @@ export const CardWorkflowField: React.FC<CardWorkflowFieldProps> = ({
isDisabled={disabled}
variant="bordered"
className="w-full"
placeholder={field.label ? `请输入${field.label.replace(/(可选)|\(可选\)/g, '').trim()}…` : undefined}
/>
{showRequiredError && (
<p className="text-[12px] text-[#f31260] mt-[6px]">
请填写
{field.label}
</p>
)}
</div>
)
}
import React, { useRef } from 'react'
import { Button } from '@heroui/react'
import type { WorkflowFormField } from '../../lib/workflowCardForm'
import { getWorkflowFieldValidationMessage } from '../../lib/workflowCardForm'
interface CardWorkflowFileFieldProps {
field: WorkflowFormField
fileName: string
disabled: boolean
isUploading?: boolean
showRequiredError: boolean
onPickFile: (file: File) => void
}
export const CardWorkflowFileField: React.FC<CardWorkflowFileFieldProps> = ({
field,
fileName,
disabled,
isUploading = false,
showRequiredError,
onPickFile,
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const label = field.required ? `*${field.label}` : field.label
const displayName = fileName.trim() || '未选择文件'
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file)
onPickFile(file)
e.target.value = ''
}
return (
<div>
<label className="block text-[14px] text-[#4b5563] mb-[6px]">{label}</label>
<div className="flex items-center gap-[12px] flex-wrap">
<span className="text-[14px] text-[#6b7280] truncate max-w-full">{displayName}</span>
<input
ref={inputRef}
type="file"
className="hidden"
disabled={disabled}
onChange={handleFileChange}
/>
<Button
size="sm"
variant="bordered"
color="primary"
isDisabled={disabled || isUploading}
isLoading={isUploading}
onPress={() => inputRef.current?.click()}
>
上传文件
</Button>
</div>
{showRequiredError && (
<p className="text-[12px] text-[#f31260] mt-[6px]">
{getWorkflowFieldValidationMessage(field)}
</p>
)}
</div>
)
}
......@@ -2,6 +2,7 @@
import React, { useState } from 'react'
import { Button, Link } from '@heroui/react'
import { motion } from 'framer-motion'
import type { WorkflowParamMap } from '../../lib/workflowCardForm'
import { CardWorkflowForm } from './CardWorkflowForm'
import type { Answer, Attachment } from '@/types/chat'
import AnswerProDetailIcon from '@/assets/svg/answerProDetail.svg?react'
......@@ -17,7 +18,7 @@ interface ChatAnswerAttachmentProps {
answer: Answer
isLastAnswer?: boolean
onSubmitQuestion?: (question: string, productCode?: string, workFlowSessionId?: string, recordId?: string) => void
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string, workFlowSessionId?: string) => void
onWorkflowSubmit?: (paramMap: WorkflowParamMap, recordId?: string, workFlowSessionId?: string) => void
isWorkflowSubmitting?: boolean
isAsking?: boolean
}
......@@ -181,7 +182,7 @@ export const ChatAnswerAttachment: React.FC<ChatAnswerAttachmentProps> = ({
answer={answer}
onSubmitWorkflow={onWorkflowSubmit || (() => { })}
isSubmitting={isWorkflowSubmitting}
isCompleted={false}
isCompleted={answer.workflowFormSubmitted === true}
/>
)}
{/* 其他 card-* 类型 */}
......
import { Avatar, Button } from '@heroui/react'
import { useEffect, useState } from 'react'
import type { WorkflowParamMap } from '../../lib/workflowCardForm'
import { ChatAnswerShower } from './ChatAnswerShower'
import { ChatAnswerParser } from './ChatAnswerParser'
import { ChatAnswerRecommend } from './ChatAnswerRecommend'
......@@ -17,9 +18,9 @@ interface ChatAnswerBoxProps {
showIndex: number
isLastAnswer: boolean
index: number
onSubmitQuestion: (question: string, productCode?: string, toolId?: string, extraParams?: { paramMap?: Record<string, string>, recordId?: string, recordType?: string, workFlowSessionId?: string, isReask?: boolean, busiType?: string }, workFlowSessionId?: string, recordId?: string) => void
onSubmitQuestion: (question: string, productCode?: string, toolId?: string, extraParams?: { paramMap?: WorkflowParamMap, recordId?: string, recordType?: string, workFlowSessionId?: string, isReask?: boolean, busiType?: string }, workFlowSessionId?: string, recordId?: string) => void
onRecommendLoadingChange?: (loading: boolean) => void
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
onWorkflowSubmit?: (paramMap: WorkflowParamMap, recordId?: string) => void
isWorkflowSubmitting?: boolean
allItems?: ChatRecord[] // 传入整个会话历史,用于查找最后一个 workFlowSessionId
}
......
import React, { useEffect, useState } from 'react'
import { Chip } from '@heroui/react'
import { resolveDisplayAnswerText } from '../../helper'
import type { WorkflowParamMap } from '../../lib/workflowCardForm'
import { ChatAnswerAttachment } from './ChatAnswerAttchment'
import { ChatAnswerOperate } from './ChatAnswerOperate'
import { formatMarkdown } from './markdownFormatter'
......@@ -17,7 +19,7 @@ interface ChatAnswerParserProps {
onSubmitQuestion: (question: string, productCode?: string, workFlowSessionId?: string, recordId?: string) => void
/** 是否在当前回答中隐藏点赞/点踩/复制按钮(例如策略页自动分析、重新分析) */
hideOperate?: boolean
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
onWorkflowSubmit?: (paramMap: WorkflowParamMap, recordId?: string) => void
isWorkflowSubmitting?: boolean
isAsking?: boolean
}
......@@ -41,7 +43,9 @@ function CheckIcon({ ...props }) {
}
export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer, onTyping, onComplate, answer, isStopTyping, onSubmitQuestion, hideOperate, onWorkflowSubmit, isWorkflowSubmitting, isAsking = false }) => {
const formatAnswer = formatMarkdown(answer.answer || '')
const displayAnswerText = resolveDisplayAnswerText(answer.answer || '')
const formatAnswer = formatMarkdown(displayAnswerText)
const hasCards = Array.isArray(answer.cardList) && answer.cardList.length > 0
const [displayedText, setDisplayedText] = useState('')
const [currentIndex, setCurrentIndex] = useState(0)
const [isTyping, setIsTyping] = useState(false)
......@@ -122,6 +126,13 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer
if (isStopTyping) {
return
}
// 仅有 cardList、无正文时跳过打字机,流式过程中即可展示卡片
if (!formatAnswer.length && hasCards) {
setIsTyping(false)
if (answer.endAnswerFlag)
onComplate()
return
}
if (!isTyping) {
onTyping()
setIsTyping(true)
......@@ -202,10 +213,7 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer
</MarkdownDetail>
</div>
)}
{!isTyping
&& answer.cardList
&& answer.cardList?.length !== 0
&& (
{hasCards && (
<ChatAnswerAttachment
isLastAnswer={isLastAnswer}
onSubmitQuestion={onSubmitQuestion}
......
import React from 'react'
import { hasWorkflowFormCards, resolveDisplayAnswerText } from '../../helper'
import type { WorkflowParamMap } from '../../lib/workflowCardForm'
import { formatMarkdown } from './markdownFormatter'
import { ChatAnswerAttachment } from './ChatAnswerAttchment'
import { ChatAnswerOperate } from './ChatAnswerOperate'
......@@ -10,7 +12,7 @@ interface ChatAnswerShowerProps {
isLastAnswer: boolean
onSubmitQuestion: (question: string, productCode?: string, workFlowSessionId?: string, recordId?: string) => void
hideOperate?: boolean
onWorkflowSubmit?: (paramMap: Record<string, string>, recordId?: string) => void
onWorkflowSubmit?: (paramMap: WorkflowParamMap, recordId?: string) => void
isWorkflowSubmitting?: boolean
isAsking?: boolean
}
......@@ -18,19 +20,27 @@ interface ChatAnswerShowerProps {
export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLastAnswer, onSubmitQuestion, hideOperate: hideOperateProp, onWorkflowSubmit, isWorkflowSubmitting, isAsking = false }) => {
const hideOperateByCard = (answer.cardList || []).some(attachment => attachment.type === 'box' || attachment?.type?.includes('card-'))
const hideOperate = hideOperateByCard || hideOperateProp
const displayAnswerText = resolveDisplayAnswerText(answer.answer || '')
const hasCards = Array.isArray(answer.cardList) && answer.cardList.length > 0
// 历史首包静默记录隐藏卡片;流式/含工作流表单卡片需正常展示
const shouldRenderCards = hasCards && !(
answer.isShow === true
&& answer.firstQaFlag === true
&& !hasWorkflowFormCards(answer.cardList)
)
return (
<div className="answerShower">
{answer.answer && (
{displayAnswerText && (
<div className={answer.cardList?.length ? 'mb-[12px] sm:mb-[20px]' : ''}>
<MarkdownDetail>
{formatMarkdown(answer.answer || '')}
{formatMarkdown(displayAnswerText)}
</MarkdownDetail>
</div>
)}
{/* 历史记录(isShow === true)且 firstQaFlag === true 时,不渲染 ChatAnswerAttachment(避免触发自动提交)
流式返回的新记录(isShow === false)正常渲染 */}
{answer.cardList && answer.cardList?.length !== 0 && !(answer.isShow === true && answer.firstQaFlag === true) && (
{shouldRenderCards && (
<ChatAnswerAttachment
onSubmitQuestion={onSubmitQuestion}
isLastAnswer={isLastAnswer}
......
......@@ -7,12 +7,16 @@ interface ChatItemUserProps {
}
export const ChatItemUser: React.FC<ChatItemUserProps> = ({ record }) => {
const question = record.question?.trim()
if (!question)
return null
return (
<div className="chatItemUser">
<div className="text-[#b9b9bb] text-[13px] text-center w-full">{record.qaTime}</div>
<div className="flex justify-end">
<div className="sm:block w-[65px] flex-shrink-0"></div>
<div className="text-[15px] bg-[#BFE9FE] rounded-[20px] box-border text-[#000000] px-[16px] py-[12px] sm:px-[24px] sm:py-[20px]">{record.question}</div>
<div className="text-[15px] bg-[#BFE9FE] rounded-[20px] box-border text-[#000000] px-[16px] py-[12px] sm:px-[24px] sm:py-[20px]">{question}</div>
<Avatar className="hidden sm:block ml-[20px] flex-shrink-0" src={AvatarUser} />
</div>
<div className="h-[20px] sm:h-[32px] w-full"></div>
......
import type { Attachment } from '@/types/chat'
/** 工作流 form_input 支持的字段类型 */
export const WORKFLOW_FIELD_TYPE = {
TEXT: 'text',
SELECT: 'select',
FILE: 'file',
} as const
export type WorkflowFieldType = typeof WORKFLOW_FIELD_TYPE[keyof typeof WORKFLOW_FIELD_TYPE] | string
/** 提交提问接口 paramMap:key 为 fieldList[].key;text/select 为 string,file 为 URL 数组 */
export type WorkflowParamMap = Record<string, string | string[]>
export interface WorkflowFieldOption {
id?: string
text?: string
type?: string
}
/** 归一化后的表单字段(来自 cardList[].content.fieldList) */
export interface WorkflowFormField {
key: string
label: string
type: WorkflowFieldType
required: boolean
multiple?: boolean
options?: WorkflowFieldOption[]
/** 接口预填(如已上传文档 URL) */
presetValue?: string
}
export type WorkflowFormValues = Record<string, string | string[]>
export interface WorkflowFileSelection {
fileName: string
file?: File
/** 上传接口返回的文件访问 URL,写入 paramMap[field.key] */
fileUrls: string[]
}
export function isSupportedWorkflowFieldType(type: string | undefined): boolean {
const normalized = String(type ?? '').trim().toLowerCase()
return normalized === WORKFLOW_FIELD_TYPE.TEXT
|| normalized === WORKFLOW_FIELD_TYPE.SELECT
|| normalized === WORKFLOW_FIELD_TYPE.FILE
}
function isHttpUrl(value: string): boolean {
return /^https?:\/\//i.test(value.trim())
}
/** 从 card-workflow 附件解析可渲染字段(保持 fieldList 顺序) */
export function parseWorkflowFormFields(attachment: Attachment): WorkflowFormField[] {
const rawList = attachment.content?.fieldList
if (!Array.isArray(rawList))
return []
return rawList
.filter(field => Boolean(field?.key) && Boolean(field?.type) && isSupportedWorkflowFieldType(field.type))
.map(field => ({
key: String(field.key).trim(),
label: String(field.label ?? field.key).trim(),
type: String(field.type).trim().toLowerCase(),
required: field.required === true,
multiple: field.multiple === true,
options: field.options,
presetValue: field.value != null ? String(field.value).trim() : undefined,
}))
}
export function isWorkflowFormAttachment(attachment: Attachment): boolean {
return attachment?.type === 'card-workflow'
&& attachment?.content?.name === 'form_input'
&& parseWorkflowFormFields(attachment).length > 0
}
export function createInitialWorkflowFormValues(
fields: WorkflowFormField[],
options?: { searchParams?: URLSearchParams, useQueryPreset?: boolean },
): WorkflowFormValues {
const initial: WorkflowFormValues = {}
const searchParams = options?.searchParams
const useQuery = options?.useQueryPreset === true && searchParams
fields.forEach((field) => {
if (field.type === WORKFLOW_FIELD_TYPE.SELECT && field.multiple) {
if (useQuery) {
const queryValue = searchParams!.get(field.key)
initial[field.key] = queryValue
? queryValue.split(',').map(item => item.trim()).filter(Boolean)
: []
}
else {
initial[field.key] = []
}
return
}
if (useQuery) {
const queryValue = searchParams!.get(field.key)
initial[field.key] = queryValue ?? field.presetValue ?? ''
return
}
initial[field.key] = field.presetValue ?? ''
})
return initial
}
export function createInitialWorkflowFileSelections(
fields: WorkflowFormField[],
): Record<string, WorkflowFileSelection | undefined> {
const selections: Record<string, WorkflowFileSelection | undefined> = {}
fields.forEach((field) => {
if (field.type !== WORKFLOW_FIELD_TYPE.FILE)
return
const preset = field.presetValue
if (preset && isHttpUrl(preset)) {
selections[field.key] = {
fileName: preset.split('/').pop() || preset,
fileUrls: [preset],
}
}
})
return selections
}
export function isWorkflowFieldFilled(
field: WorkflowFormField,
value: string | string[] | undefined,
fileSelection?: WorkflowFileSelection,
): boolean {
if (field.type === WORKFLOW_FIELD_TYPE.FILE) {
if ((fileSelection?.fileUrls?.length ?? 0) > 0)
return true
if (field.presetValue && isHttpUrl(field.presetValue))
return true
return false
}
if (field.type === WORKFLOW_FIELD_TYPE.SELECT && field.multiple) {
return Array.isArray(value) && value.length > 0
}
if (typeof value === 'string')
return value.trim() !== ''
return false
}
export function getWorkflowRequiredMissingFields(
fields: WorkflowFormField[],
values: WorkflowFormValues,
fileSelections: Record<string, WorkflowFileSelection | undefined>,
): WorkflowFormField[] {
return fields.filter(
field => field.required && !isWorkflowFieldFilled(field, values[field.key], fileSelections[field.key]),
)
}
function buildTextOrSelectParamValue(field: WorkflowFormField, value: string | string[] | undefined): string {
if (field.type === WORKFLOW_FIELD_TYPE.SELECT && field.multiple) {
return Array.isArray(value) ? value.join(',') : ''
}
return typeof value === 'string' ? value : ''
}
function buildFileParamValue(
field: WorkflowFormField,
fileSelection?: WorkflowFileSelection,
): string[] {
const urls = fileSelection?.fileUrls?.filter(url => url.trim()) ?? []
if (urls.length > 0)
return urls
if (field.presetValue && isHttpUrl(field.presetValue))
return [field.presetValue]
return []
}
/**
* 按 cardList.fieldList 的 key 构建 paramMap(与 submit_question_stream 约定一致)
* - text / select → string(未填为 "")
* - file → string[](未填为 [],已上传为 ["http://..."])
*/
export function buildWorkflowParamMap(
fields: WorkflowFormField[],
values: WorkflowFormValues,
fileSelections: Record<string, WorkflowFileSelection | undefined>,
): WorkflowParamMap {
const paramMap: WorkflowParamMap = {}
fields.forEach((field) => {
if (field.type === WORKFLOW_FIELD_TYPE.FILE)
paramMap[field.key] = buildFileParamValue(field, fileSelections[field.key])
else
paramMap[field.key] = buildTextOrSelectParamValue(field, values[field.key])
})
return paramMap
}
/** 解析上传接口返回中的文件 URL */
export function extractUploadUrlFromResponse(data: unknown): string {
if (typeof data === 'string' && data.trim())
return data.trim()
if (Array.isArray(data)) {
const first = data.find(item => typeof item === 'string' && item.trim())
return typeof first === 'string' ? first.trim() : ''
}
if (data && typeof data === 'object') {
const record = data as Record<string, unknown>
for (const key of ['url', 'fileUrl', 'docUrl', 'path', 'filePath']) {
const candidate = record[key]
if (typeof candidate === 'string' && candidate.trim())
return candidate.trim()
}
if (Array.isArray(record.urlList)) {
return extractUploadUrlFromResponse(record.urlList)
}
}
return ''
}
/**
* 构建 paramMap;若 file 已选本地文件但未上传,会先调用 uploadFile 再写入 URL 数组
*/
export async function buildWorkflowParamMapAsync(
fields: WorkflowFormField[],
values: WorkflowFormValues,
fileSelections: Record<string, WorkflowFileSelection | undefined>,
uploadFile?: (file: File) => Promise<string>,
): Promise<WorkflowParamMap> {
const nextSelections = { ...fileSelections }
for (const field of fields) {
if (field.type !== WORKFLOW_FIELD_TYPE.FILE || !uploadFile)
continue
const selection = nextSelections[field.key]
if (!selection?.file || (selection.fileUrls?.length ?? 0) > 0)
continue
const url = await uploadFile(selection.file)
if (url) {
nextSelections[field.key] = {
...selection,
fileUrls: [url],
}
}
}
return buildWorkflowParamMap(fields, values, nextSelections)
}
export function getWorkflowFieldValidationMessage(field: WorkflowFormField): string {
if (field.type === WORKFLOW_FIELD_TYPE.FILE)
return `请上传${field.label.replace(/(可选)|\(可选\)/g, '').trim() || '文件'}`
if (field.type === WORKFLOW_FIELD_TYPE.SELECT)
return `请选择${field.label}`
return `请填写${field.label}`
}
......@@ -137,7 +137,7 @@ const conversationSlice = createSlice({
state.businessInsightWorkflow.isInSubWorkflow = true
state.businessInsightWorkflow.paramMap = {}
},
updateBusinessInsightParamMap: (state, action: PayloadAction<Record<string, string>>) => {
updateBusinessInsightParamMap: (state, action: PayloadAction<Record<string, string | string[]>>) => {
state.businessInsightWorkflow.paramMap = {
...state.businessInsightWorkflow.paramMap,
...action.payload,
......
......@@ -91,6 +91,8 @@ export interface Answer {
qaTime?: number | string
firstQaFlag?: boolean
workFlowSessionId?: string
/** 工作流表单已提交(paramMap 已发往流式接口) */
workflowFormSubmitted?: boolean
}
export interface OriginalRecord {
......
......@@ -20,7 +20,7 @@ export interface ConversationState {
mainWorkflowSessionId: string | null
subWorkflowSessionId: string | null
subWorkflowRecordType: string | null
paramMap: Record<string, string>
paramMap: Record<string, 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