diff --git a/web/src/components/Chat/ChatContent.tsx b/web/src/components/Chat/ChatContent.tsx index a1439746..34472a2e 100644 --- a/web/src/components/Chat/ChatContent.tsx +++ b/web/src/components/Chat/ChatContent.tsx @@ -2,15 +2,15 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:46:17 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-23 18:24:33 + * @Last Modified time: 2026-03-26 13:32:29 */ import { type FC, useRef, useEffect, useState } from 'react' import clsx from 'clsx' import Markdown from '@/components/Markdown' import type { ChatContentProps } from './types' -import { Spin, Divider, Space, Image, Flex } from 'antd' +import { Spin, Divider, Space, Image, Flex, Button } from 'antd' import { SoundOutlined } from '@ant-design/icons' - +import { t } from 'i18next' const getFileUrl = (file: any) => { return file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined) @@ -29,7 +29,8 @@ const ChatContent: FC = ({ labelPosition = 'bottom', labelFormat, errorDesc, - renderRuntime + renderRuntime, + onSend }) => { // Scroll container reference for controlling auto-scroll to bottom const scrollContainerRef = useRef<(HTMLDivElement | null)>(null) @@ -178,6 +179,25 @@ const ChatContent: FC = ({ {/* Render message content using Markdown component */} + {item.meta_data?.suggested_questions && item.meta_data?.suggested_questions?.length > 0 && + {item.meta_data?.suggested_questions?.map((question, idx) => ( + + ))} + } + {item.meta_data?.citations && item.meta_data?.citations.length > 0 &&
+
{t('memoryConversation.citations')}
+ {item.meta_data?.citations?.map((citation, idx) => ( + + ))} +
} {item.meta_data?.audio_url && <> diff --git a/web/src/components/Chat/index.tsx b/web/src/components/Chat/index.tsx index 521425f7..49feaf33 100644 --- a/web/src/components/Chat/index.tsx +++ b/web/src/components/Chat/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:46:09 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-19 20:47:27 + * @Last Modified time: 2026-03-26 13:32:46 */ import { type FC } from 'react' import ChatInput from './ChatInput' @@ -40,6 +40,7 @@ const Chat: FC = ({ labelFormat={labelFormat} errorDesc={errorDesc} renderRuntime={renderRuntime} + onSend={onSend} /> {/* Chat input area */} diff --git a/web/src/components/Chat/types.ts b/web/src/components/Chat/types.ts index ceb42c3b..312dbe50 100644 --- a/web/src/components/Chat/types.ts +++ b/web/src/components/Chat/types.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:45:54 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-23 18:15:05 + * @Last Modified time: 2026-03-26 12:30:51 */ import { type ReactNode } from 'react' @@ -27,6 +27,13 @@ export interface ChatItem { audio_url?: string; audio_status?: string; files?: any[]; + suggested_questions?: string[]; + citations?: { + document_id: string; + file_name: string; + knowledge_id: string; + score: string; + }[] }, } @@ -101,4 +108,6 @@ export interface ChatContentProps { labelFormat: (item: ChatItem) => any; errorDesc?: string; renderRuntime?: (item: ChatItem, index: number) => ReactNode; + /** Send message callback */ + onSend?: (msg: string) => void; } \ No newline at end of file diff --git a/web/src/components/SortableList/index.tsx b/web/src/components/SortableList/index.tsx index 1ac27a82..339d507d 100644 --- a/web/src/components/SortableList/index.tsx +++ b/web/src/components/SortableList/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:27:36 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:27:36 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-26 12:02:23 */ /** * SortableList Component diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 0900e8d8..17fcde8d 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1412,6 +1412,14 @@ export const en = { unix: 'items', text_to_speech: 'Text to Speech', text_to_speech_desc: 'Text can be converted to speech', + opening_statement: 'Conversation Opening', + opening_statement_desc: 'Set the conversation opening content', + editOpeningStatement: 'Edit Opening Statement', + suggested_questions: 'Opening Questions', + add_questions: 'Add Option', + citation: 'Citation and Attribution', + citation_desc: 'Display the attribution of source documents and generated content', + invalidVariablesTitle: "The following undefined variables are referenced in the conversation opening. Do you want to save the opening configuration?", apps: 'My Apps', sharing: 'Sharing', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 140e3009..d783bea3 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -787,6 +787,14 @@ export const zh = { unix: '个', text_to_speech: '文字转语音', text_to_speech_desc: '文本可以转换成语音', + opening_statement: '对话开场白', + opening_statement_desc: '设置对话开场白内容', + editOpeningStatement: '编辑开场白', + suggested_questions: '开场问题', + add_questions: '添加选项', + citation: '引用和归属', + citation_desc: '显示源文档和生成内容的归属部分', + invalidVariablesTitle: "对话开场白中引用了以下未定义的变量,是否保存开场白配置?", apps: '我的应用', sharing: '共享', @@ -1815,6 +1823,7 @@ export const zh = { memoryTipTitle: '确定打开对话记忆功能吗?打开后对话将会保存到记忆库中', stopAudioRecorder: '停止录音', startAudioRecorder: '开始录音', + citations: '引用', }, login: { title: '红熊记忆科学', diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index da978660..0d272c1d 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:21 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-25 16:32:26 + * @Last Modified time: 2026-03-26 12:13:33 */ import { useEffect, useRef, useState, forwardRef, useImperativeHandle, useMemo } from 'react'; import { useTranslation } from 'react-i18next' @@ -335,8 +335,24 @@ const Agent = forwardRef { - setChatVariables(values) + const handleSaveChatVariable = (variables: Variable[]) => { + setChatVariables(variables) + const opening_statement = form.getFieldValue(['features', 'opening_statement']) + + if (opening_statement?.statement && opening_statement?.statement.trim() !== '') { + const statement = opening_statement.statement as string + const replacedContent = statement.replace(/\{\{([^}]+)\}\}/g, (match, name) => { + const v = variables.find(item => item.name === name) + return v?.value != null && v.value !== '' ? String(v.value) : match + }) + setChatList(prev => prev.map(item => { + const list = [...(item.list || [])] + if (list.length > 0 && list[0].role === 'assistant') { + list[0] = { ...list[0], content: replacedContent } + } + return { ...item, list } + })) + } } useEffect(() => { setChatVariables(values?.variables || []) @@ -344,11 +360,36 @@ const Agent = forwardRef { form.setFieldValue('features', value) + + if (value?.opening_statement?.statement && value?.opening_statement?.statement.trim() !== '') { + setChatList(prev => (prev.map(item => { + const firstMsg = item.list?.[0] + + if (firstMsg?.role === 'assistant') { + firstMsg.meta_data = { + suggested_questions: value.opening_statement?.suggested_questions || [] + } + return item + } else { + return { + ...item, + list: [{ + role: 'assistant', + content: value.opening_statement?.statement, + meta_data: { + suggested_questions: value.opening_statement?.suggested_questions || [] + } + }, ...(item.list || [])] + } + } + }))) + } } const modelLogo = useMemo(() => { return defaultModel?.name && getListLogoUrl(defaultModel.provider, defaultModel.logo as string) }, [defaultModel]) - console.log('values', values, defaultModel) + + console.log('agent values', values) return ( <> {loading && } @@ -365,7 +406,12 @@ const Agent = forwardRef - + @@ -393,19 +439,19 @@ const Agent = forwardRef ({t('application.configurationDesc')}) - - - - + + + + diff --git a/web/src/views/ApplicationConfig/TestChat/index.tsx b/web/src/views/ApplicationConfig/TestChat/index.tsx index af291d1f..fd20ab1e 100644 --- a/web/src/views/ApplicationConfig/TestChat/index.tsx +++ b/web/src/views/ApplicationConfig/TestChat/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-13 17:27:52 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-24 10:19:31 + * @Last Modified time: 2026-03-26 13:43:02 */ import { type FC, useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -88,11 +88,31 @@ const TestChat: FC = ({ getVariables() }, [application, config]) + useEffect(() => { + return () => { + audioPollingRef.current.forEach(timer => clearInterval(timer)) + audioPollingRef.current.clear() + } + }, []) + const getVariables = () => { if (!application || !config) return setFeatures(config?.features || {} as FeaturesConfigForm) + + if (config?.features?.opening_statement?.statement && config?.features?.opening_statement?.statement.trim() !== '') { + setChatList(prev => [...prev, { + role: 'assistant', + created_at: Date.now(), + content: config?.features?.opening_statement?.statement, + meta_data: { + suggested_questions: config?.features?.opening_statement?.suggested_questions || [] + } + }]) + } + + let initVariables: Variable[] = [] switch (application.type) { @@ -142,7 +162,7 @@ const TestChat: FC = ({ }]) } - const updateAssistantMessage = (content: string, audio_url?: string) => { + const updateAssistantMessage = (content: string, audio_url?: string, audio_status?: string, citations?: any[]) => { setChatList(prev => { const newList = [...prev] const lastMsg = newList[newList.length - 1] @@ -150,7 +170,11 @@ const TestChat: FC = ({ newList[newList.length - 1] = { ...lastMsg, content: lastMsg.content + content, - ...(audio_url !== undefined ? { meta_data: { ...lastMsg.meta_data, audio_url, audio_status: 'pending' } } : {}) + meta_data: { + audio_url: audio_url || lastMsg.meta_data?.audio_url, + audio_status: audio_status || lastMsg.meta_data?.audio_status, + citations: citations || lastMsg.meta_data?.citations + } } } return newList @@ -188,14 +212,14 @@ const TestChat: FC = ({ return { isCanSend, params } } - const handleSend = () => { - if (loading || !application || !message || !message?.trim()) return + const handleSend = (msg?: string) => { + if (loading || !application || !((message && message?.trim() !== '') || (msg && msg?.trim() !== ''))) return const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status)) const variables = toolbarRef.current?.getVariables() || [] const { isCanSend, params } = buildVariableParams(variables) if (!isCanSend) return - addUserMessage(message, files) + addUserMessage((msg || message) as string, files) setMessage(undefined) toolbarRef.current?.setFiles([]) setFileList([]) @@ -205,7 +229,7 @@ const TestChat: FC = ({ draftRun( application.id, - formatParams(message, conversationId, files, params), + formatParams((msg || message) as string, conversationId, files, params), handleStreamMessage ) .catch(() => { @@ -236,7 +260,15 @@ const TestChat: FC = ({ const handleStreamMessage = (data: SSEMessage[]) => { data.map(item => { - const { conversation_id, content, message_length, audio_url } = item.data as { conversation_id: string, content: string, message_length: number; audio_url?: string; }; + const { conversation_id, content, message_length, audio_url, citations } = item.data as { + conversation_id: string, content: string, message_length: number; audio_url?: string; + citations?: { + document_id: string; + file_name: string; + knowledge_id: string; + score: string; + }[] + }; switch (item.event) { case 'start': if (conversation_id && conversationId !== conversation_id) setConversationId(conversation_id) @@ -253,7 +285,7 @@ const TestChat: FC = ({ })) } if (audio_url) { - updateAssistantMessage(content || '', audio_url) + updateAssistantMessage(content || '', audio_url, 'pending') const { file_id } = item.data as { file_id?: string } const idToPoll = file_id || audio_url || '' const fileId = audio_url.split('/').pop() @@ -279,6 +311,9 @@ const TestChat: FC = ({ audioPollingRef.current.set(idToPoll, timer) } } + if (citations && citations.length > 0) { + updateAssistantMessage(content, audio_url, undefined, citations) + } updateErrorAssistantMessage(message_length) setStreamLoading(false) break diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index 7608c238..940fe7ab 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:27:39 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-24 10:12:09 + * @Last Modified time: 2026-03-26 13:41:44 */ /** * Chat debugging component for application testing @@ -77,8 +77,19 @@ const Chat: FC = ({ useEffect(() => { setCompareLoading(false) setLoading(false) + return () => { + audioPollingRef.current.forEach(timer => clearInterval(timer)) + audioPollingRef.current.clear() + } }, [chatList.map(item => item.label).join(',')]) + useEffect(() => { + return () => { + audioPollingRef.current.forEach(timer => clearInterval(timer)) + audioPollingRef.current.clear() + } + }, []) + useEffect(() => { if (data?.features) setFeatures(data.features) }, [data?.features]) @@ -130,8 +141,8 @@ const Chat: FC = ({ } } /** Update assistant message with streaming content */ - const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string, audio_url?: string) => { - if ((!content && !audio_url) || !model_config_id) return + const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string, audio_url?: string, citations?: any[]) => { + if ((!content && !audio_url && (!citations || citations?.length < 1)) || !model_config_id) return updateChatList(prev => { const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id); if (targetIndex !== -1) { @@ -148,7 +159,10 @@ const Chat: FC = ({ { ...lastMsg, content: lastMsg.content + (content || ''), - ...(audio_url !== undefined ? { meta_data: { audio_url, audio_status: 'pending' } } : {}) + meta_data: { + ...(audio_url !== undefined ? { audio_url, audio_status: 'pending' } : {}), + citations: citations || lastMsg.meta_data?.citations + } } ] } @@ -249,7 +263,15 @@ const Chat: FC = ({ setCompareLoading(false) data.map(item => { - const { model_config_id, conversation_id, content, message_length, audio_url } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number; audio_url: string }; + const { model_config_id, conversation_id, content, message_length, audio_url, citations } = item.data as { + model_config_id: string; conversation_id: string; content: string; message_length: number; audio_url: string; + citations?: { + document_id: string; + file_name: string; + knowledge_id: string; + score: string; + }[] + }; switch (item.event) { case 'model_message': @@ -264,7 +286,7 @@ const Chat: FC = ({ })) } if (audio_url) { - updateAssistantMessage(content, model_config_id, conversation_id, audio_url) + updateAssistantMessage(content, model_config_id, conversation_id, audio_url, citations) const fileId = audio_url.split('/').pop() if (fileId && idToPoll && !audioPollingRef.current.has(idToPoll)) { const timer = setInterval(() => { @@ -289,6 +311,10 @@ const Chat: FC = ({ audioPollingRef.current.set(idToPoll, timer) } } + + if (citations && citations.length > 0) { + updateAssistantMessage(content, model_config_id, conversation_id, audio_url, citations) + } updateErrorAssistantMessage(message_length, model_config_id) break; case 'compare_end': @@ -481,6 +507,8 @@ const Chat: FC = ({ const handleDelete = (index: number) => { updateChatList(chatList.filter((_, voIndex) => voIndex !== index)) } + + console.log('chatList', chatList) const isHasLabel = useMemo(() => chatList.some(item => item.label), [chatList]) const isNeedVariableConfig = useMemo(() => chatVariables?.some(vo => vo.required && !vo.value), [chatVariables]) return ( @@ -539,6 +567,7 @@ const Chat: FC = ({ "rb:h-[calc(100vh-292px)]": !isHasLabel, })} />} + onSend={isCluster ? handleClusterSend : handleSend} data={chat.list || []} streamLoading={compareLoading} labelPosition="top" diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/FeaturesConfigModal.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/FeaturesConfigModal.tsx index cc2accf2..e8761662 100644 --- a/web/src/views/ApplicationConfig/components/FeaturesConfig/FeaturesConfigModal.tsx +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/FeaturesConfigModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:27:56 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-24 10:59:37 + * @Last Modified time: 2026-03-26 14:03:01 */ /** * Copy Application Modal @@ -20,11 +20,14 @@ import SwitchFormItem from '@/components/FormItem/SwitchFormItem' import FileUploadSettingModal from './FileUploadSettingModal' import type { Application } from '@/views/ApplicationManagement/types'; import type { Capability } from '@/views/ModelManagement/types' +import OpenStatementSettingModal, { type OpenStatementSettingModalRef } from './OpenStatementSettingModal' +import type { Variable } from '../VariableList/types' interface FeaturesConfigModalProps { refresh: (value: FeaturesConfigForm) => void; source?: Application['type']; capability?: Capability[]; + chatVariables: Variable[]; } const max_file_count = 1; /** @@ -34,12 +37,14 @@ const FeaturesConfigModal = forwardRef { const { t } = useTranslation(); const [visible, setVisible] = useState(false); const [form] = Form.useForm(); const values = Form.useWatch([], form) const fileUploadSettingModalRef = useRef(null) + const openStatementSettingModalRef = useRef(null) /** Close modal and reset form */ const handleClose = () => { @@ -54,8 +59,10 @@ const FeaturesConfigModal = forwardRef { - setVisible(false); - refresh(form.getFieldsValue()) + form.validateFields().then((values) => { + setVisible(false); + refresh(values) + }) } const handleOpenSettings = () => { @@ -82,6 +89,13 @@ const FeaturesConfigModal = forwardRef item.enabled) } + const handleOpenStatementSettings = () => { + openStatementSettingModalRef.current?.handleOpen(values?.opening_statement) + } + const handleSaveStatement = (settings: FeaturesConfigForm['opening_statement']) => { + form.setFieldValue('opening_statement', settings) + } + /** Expose methods to parent component */ useImperativeHandle(ref, () => ({ handleOpen, @@ -103,6 +117,23 @@ const FeaturesConfigModal = forwardRef {source !== 'workflow' && <> +
+ + {values?.opening_statement?.enabled && (() => { + const statement = values.opening_statement?.statement + return statement && statement.trim() !== '' ? <> +
+ {statement} +
+ + : + })()} +
+
+ +
}
@@ -129,7 +167,6 @@ const FeaturesConfigModal = forwardRef 0 ? <>
@@ -165,6 +202,11 @@ const FeaturesConfigModal = forwardRef + ); }); diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx new file mode 100644 index 00000000..b85a9006 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx @@ -0,0 +1,125 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-05 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-26 14:12:11 + */ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Button, Form, Input, Flex, App } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import RbModal from '@/components/RbModal'; +import type { FeaturesConfigForm } from '../../types' +import type { Variable } from '../VariableList/types' +import Tag from '@/components/Tag' + +export interface OpenStatementSettingModalRef { + handleOpen: (values?: FeaturesConfigForm['opening_statement']) => void; + handleClose: () => void; +} + +interface OpenStatementSettingModalProps { + onSave: (values: FeaturesConfigForm['opening_statement']) => void; + chatVariables?: Variable[]; +} + +const OpenStatementSettingModal = forwardRef(({ + onSave, + chatVariables = [] +}, ref) => { + const { t } = useTranslation(); + const { modal } = App.useApp() + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + + const handleClose = () => { + setVisible(false); + form.resetFields(); + }; + + const handleOpen = (values?: FeaturesConfigForm['opening_statement']) => { + setVisible(true); + form.setFieldsValue(values || {}); + }; + + const handleSave = async () => { + form.validateFields().then(values => { + if (values?.enabled && values?.statement && values?.statement?.trim() !== '') { + const usedVars = [...new Set([...values.statement?.matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1]))] + + const validNames = new Set(chatVariables.map(v => v.name)) + const invalid = usedVars.filter(v => !validNames.has(v)) + console.log('invalid', invalid) + if (invalid.length > 0) { + modal.confirm({ + title: t('application.invalidVariablesTitle'), + content: invalid.map((vo, index) => {'{{'}{vo}{'}}'}), + okText: t('common.confirm'), + cancelText: t('common.cancel'), + onOk: () => { + onSave(values); + handleClose(); + }, + }) + } else { + onSave(values); + handleClose(); + } + } + }); + }; + + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+
+ ); +}); + +export default OpenStatementSettingModal; diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/index.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/index.tsx index e3847fd3..e27c2b86 100644 --- a/web/src/views/ApplicationConfig/components/FeaturesConfig/index.tsx +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/index.tsx @@ -12,6 +12,7 @@ import FeaturesConfigModal from './FeaturesConfigModal' import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types' import type { Application } from '@/views/ApplicationManagement/types'; import type { Capability } from '@/views/ModelManagement/types' +import type { Variable } from '../VariableList/types' /** Props for the FeaturesConfig component */ interface FeaturesConfigProps { @@ -21,13 +22,15 @@ interface FeaturesConfigProps { refresh: (value: FeaturesConfigForm) => void; source?: Application['type']; capability?: Capability[]; + chatVariables: Variable[]; } const FeaturesConfig: FC = ({ value, refresh, source, - capability + capability, + chatVariables }) => { const { t } = useTranslation(); // Ref used to imperatively open the config modal @@ -50,6 +53,7 @@ const FeaturesConfig: FC = ({ refresh={refresh} source={source} capability={capability} + chatVariables={chatVariables} /> ) diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index b1e3749c..3e64dfe1 100644 --- a/web/src/views/Conversation/index.tsx +++ b/web/src/views/Conversation/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:58:03 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-24 10:19:34 + * @Last Modified time: 2026-03-26 13:35:42 */ /** * Conversation Page @@ -142,6 +142,8 @@ const Conversation: FC = () => { } useEffect(() => { + audioPollingRef.current.forEach((timer) => clearInterval(timer)) + audioPollingRef.current.clear() if (conversation_id) { getConversationDetail(token as string, conversation_id) .then(res => { @@ -149,9 +151,20 @@ const Conversation: FC = () => { setChatList(response?.messages || []) }) } else { - setChatList([]) + if (features?.opening_statement?.statement) { + setChatList([{ + role: 'assistant', + content: features.opening_statement.statement, + created_at: Date.now(), + meta_data: { + suggested_questions: features.opening_statement?.suggested_questions + } + }]) + } else { + setChatList([]) + } } - }, [conversation_id]) + }, [conversation_id, features?.opening_statement?.statement]) const addUserMessage = (message: string = '', files?: any[]) => { setChatList(prev => [...prev, { @@ -173,8 +186,8 @@ const Conversation: FC = () => { }]) } - const updateAssistantMessage = (content: string = '', audio_url?: string, audio_status?: string) => { - if (!content && !audio_url) return + const updateAssistantMessage = (content: string = '', audio_url?: string, audio_status?: string, citations?: any[]) => { + if (!content && !audio_url && (!citations || citations?.length < 1)) return if (streamLoading) setStreamLoading(false) setChatList(prev => { const lastList = [...prev] @@ -186,7 +199,11 @@ const Conversation: FC = () => { { ...lastMsg, content: lastMsg.content + content, - meta_data: { audio_url, audio_status } + meta_data: { + audio_url: audio_url || lastMsg.meta_data?.audio_url, + audio_status: audio_status || lastMsg.meta_data?.audio_status, + citations: citations || lastMsg.meta_data?.citations + } } ] } @@ -210,7 +227,7 @@ const Conversation: FC = () => { }, [audioStatusMap, chatList.length]) /** Send message and handle streaming response */ - const handleSend = () => { + const handleSend = (msg?: string) => { if (!token || !shareToken) return const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status)) const variables = toolbarRef.current?.getVariables() || [] @@ -234,7 +251,7 @@ const Conversation: FC = () => { setLoading(true) setStreamLoading(true) - addUserMessage(message, files) + addUserMessage(msg || message, files) addAssistantMessage() toolbarRef.current?.setFiles([]) setFileList([]) @@ -242,7 +259,15 @@ const Conversation: FC = () => { let currentConversationId: string | null = null const handleStreamMessage = (data: SSEMessage[]) => { data.forEach((item) => { - const { content, conversation_id: curId, audio_url } = item.data as { content: string; conversation_id: string; audio_url?: string; } + const { content, conversation_id: curId, audio_url, citations } = item.data as { + content: string; conversation_id: string; audio_url?: string; + citations?: { + document_id: string; + file_name: string; + knowledge_id: string; + score: string; + }[] + } switch (item.event) { case 'start': case 'node_start': @@ -256,7 +281,7 @@ const Conversation: FC = () => { case 'end': case 'workflow_end': if (audio_url) { - updateAssistantMessage(content, audio_url, 'pending') + updateAssistantMessage(content, audio_url, 'pending', citations) const { file_id } = item.data as { file_id?: string } const idToPoll = file_id || audio_url || '' const fileId = audio_url.split('/').pop() @@ -273,21 +298,33 @@ const Conversation: FC = () => { })) clearInterval(audioPollingRef.current.get(idToPoll)) audioPollingRef.current.delete(idToPoll) + getHistory(true) + if (currentConversationId && currentConversationId !== conversation_id) { + setConversationId(currentConversationId) + } } }) .catch(() => { clearInterval(audioPollingRef.current.get(idToPoll)) audioPollingRef.current.delete(idToPoll) + getHistory(true) + if (currentConversationId && currentConversationId !== conversation_id) { + setConversationId(currentConversationId) + } }) }, 2000) audioPollingRef.current.set(idToPoll, timer) } + } else { + getHistory(true) + if (currentConversationId && currentConversationId !== conversation_id) { + setConversationId(currentConversationId) + } + } + if (citations && citations.length > 0) { + updateAssistantMessage(content, audio_url, undefined, citations) } setLoading(false) - if (currentConversationId && currentConversationId !== conversation_id) { - setConversationId(currentConversationId) - } - getHistory(true) break } }) @@ -296,7 +333,7 @@ const Conversation: FC = () => { sendConversation({ web_search: webSearch, memory, - message: message || '', + message: msg || message || '', stream: true, conversation_id: conversation_id || null, files: files.map(file => { @@ -338,6 +375,8 @@ const Conversation: FC = () => { }) } + console.log('chatList', chatList) + return (