Merge branch 'develop' into feature/ui_upgrade_zy

This commit is contained in:
zhaoying
2026-03-23 15:06:53 +08:00
71 changed files with 1766 additions and 763 deletions

View File

@@ -183,7 +183,7 @@ const TestChat: FC<TestChatProps> = ({
const handleSend = () => {
if (loading || !application || !message || !message?.trim()) return
const files = toolbarRef.current?.getFiles() || []
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
@@ -235,7 +235,7 @@ const TestChat: FC<TestChatProps> = ({
const handleWorkflowSend = () => {
if (loading || !application || !message || !message?.trim()) return
const files = toolbarRef.current?.getFiles() || []
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

View File

@@ -189,7 +189,7 @@ const Chat: FC<ChatProps> = ({
.then(() => {
const message = msg
if (!message?.trim()) return
const files = toolbarRef.current?.getFiles() || []
const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
// Validate required variables before sending
let isCanSend = true
const params: Record<string, any> = {}
@@ -350,7 +350,7 @@ const Chat: FC<ChatProps> = ({
.then(() => {
const message = msg
if (!message || message.trim() === '') return
const files = toolbarRef.current?.getFiles() || []
const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
addUserMessage(message, files)
setMessage(undefined)
toolbarRef.current?.setFiles([])

View File

@@ -24,7 +24,7 @@ interface FeaturesConfigModalProps {
refresh: (value: FeaturesConfigForm) => void;
source?: Application['type'];
}
const max_file_count = 1;
/**
* Modal for copying applications
*/
@@ -133,7 +133,7 @@ const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigMod
</div>
<div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:py-1">{t('application.maxCount')}</div>
{fu.max_file_count} {t('application.unix')}
{max_file_count} {t('application.unix')}
</div>
</Flex>
<Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-03-05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 15:18:20
* @Last Modified time: 2026-03-19 20:19:14
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd';
@@ -82,28 +82,27 @@ const defaultValues: FileUpload = {
"mp3",
"wav",
"m4a",
"ogg",
"flac"
],
document_enabled: false,
document_max_size_mb: 100,
document_allowed_extensions: [
"pdf",
"docx",
"doc",
"xlsx",
"xls",
"txt",
"csv",
"json"
"json",
"md",
],
video_enabled: false,
video_max_size_mb: 100,
video_allowed_extensions: [
"mp4",
"mov",
"avi",
"webm"
],
max_file_count: 5,
max_file_count: 1,
allowed_transfer_methods: 'both'
}
@@ -168,8 +167,8 @@ const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadS
</Radio.Group>
</Form.Item>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
<Form.Item label={t('application.maxCount')} name="max_file_count">
{/* <div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div> */}
<Form.Item label={t('application.maxCount')} name="max_file_count" hidden>
<InputNumber min={1} max={20} precision={0} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
</Form.Item>

View File

@@ -23,7 +23,7 @@
import { useState, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react';
import { Upload, Progress, App, Flex } from 'antd';
import type { UploadProps, UploadFile } from 'antd';
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
import type { UploadProps as RcUploadProps, RcFile, UploadFileStatus } from 'antd/es/upload/interface';
import { useTranslation } from 'react-i18next';
import { request } from '@/utils/request'
@@ -221,17 +221,29 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
*/
const handleCustomRequest: RcUploadProps['customRequest'] = async (options) => {
const { file, onSuccess, onError } = options;
try {
const formData = new FormData();
formData.append('file', file);
const response = await request.uploadFile(action, formData, requestConfig);
onSuccess?.({data: response});
} catch (error) {
onError?.(error as Error);
if (typeof file === 'string') return;
const rcFile = file as RcFile;
const formData = new FormData();
formData.append('file', rcFile);
const fileVo: UploadFile = {
uid: rcFile.uid,
name: rcFile.name,
status: 'uploading' as UploadFileStatus,
percent: 0,
type: rcFile.type,
originFileObj: rcFile,
thumbUrl: URL.createObjectURL(rcFile)
}
onChange?.(fileVo)
request.uploadFile(action, formData, requestConfig)
.then(res => {
onSuccess?.({ data: res });
})
.catch((error) => {
onError?.(error as Error);
fileVo.status = 'error'
onChange?.(fileVo)
})
};
/**

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:09:47
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 21:10:01
* @Last Modified time: 2026-03-19 20:32:32
*/
/**
* Upload File List Modal Component
@@ -19,7 +19,10 @@
* @component
*/
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
import { Form, Input, Select, Button, Flex } from 'antd';
import { Form, Input, Select,
// Button,
Flex
} from 'antd';
import { useTranslation } from 'react-i18next';
import type { UploadFileListModalRef } from '../types'
@@ -105,9 +108,11 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
onOk={handleSave}
confirmLoading={loading}
>
<Form form={form} layout="vertical">
<Form form={form} layout="vertical" initialValues={{ files: [{ type: undefined, url: undefined }] }}>
<Form.List name="files">
{(fields, { add, remove }) => (
{(fields,
// { add, remove }
) => (
<>
{/* Render each file entry with type selector and URL input */}
{fields.map(({ key, name, ...restField }) => (
@@ -116,6 +121,9 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
{...restField}
name={[name, 'type']}
className="rb:mb-0!"
rules={[
{ required: true, message: t('common.pleaseSelect') }
]}
>
<Select
placeholder={t('memoryConversation.fileType')}
@@ -126,22 +134,25 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
<FormItem
{...restField}
name={[name, 'url']}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ type: 'url', message: t('common.callbackUrlInvalid') },
]}
className="rb:mb-0! rb:flex-1!"
>
<Input placeholder={t('memoryConversation.fileUrl')} />
</FormItem>
<div
{/* <div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => remove(name)}
></div>
></div> */}
</Flex>
))}
<Form.Item noStyle>
{/* <Form.Item noStyle>
<Button type="dashed" onClick={() => add()} block>
+ {t('common.add')}
</Button>
</Form.Item>
</Form.Item> */}
</>
)}
</Form.List>

View File

@@ -194,7 +194,7 @@ const Conversation: FC = () => {
/** Send message and handle streaming response */
const handleSend = () => {
if (!token || !shareToken) return
const files = toolbarRef.current?.getFiles() || []
const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
const variables = toolbarRef.current?.getVariables() || []
let isCanSend = true
const params: Record<string, any> = {}

View File

@@ -11,7 +11,7 @@ import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager';
import { Button, Spin, message, Switch } from 'antd';
import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '@/api/knowledgeBase';
import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk, getFileUrl } from '@/api/knowledgeBase';
import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types';
import { formatDateTime } from '@/utils/format';
import InfoPanel, { type InfoItem } from '../components/InfoPanel';
@@ -138,7 +138,7 @@ const DocumentDetails: FC = () => {
const response = await getDocumentDetail(documentId);
setDocument(response);
setInfoItems(formatDocumentInfo(response));
const url = `${imagePath}/api/files/${response.file_id}`
const url = `${window.location.origin}/api/files/${response.file_id}`;
setFileUrl(url);
setParserMode(response?.parser_config?.auto_questions || 0)
// ChunkList will be called automatically in useEffect based on document.progress

View File

@@ -191,24 +191,28 @@ const RelationshipNetwork: FC = () => {
})}>
{(selectedNode as RawCommunityNode).properties.community_id
? <div>
<div className="rb:font-medium rb:text-[#212332] rb:text-[16px] rb:leading-5.5 rb:pl-1">
{(selectedNode as RawCommunityNode).properties.name}
</div>
<div className="rb:mt-3 rb:font-medium rb:leading-5 rb:pl-1">{t('userMemory.summary')}</div>
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:px-3 rb:py-2.5 rb:mt-2">
{(selectedNode as RawCommunityNode).properties.summary}
</div>
<Flex align="center" justify="space-between" className="rb:mt-5!">
<span className="rb:text-[#5B6167] rb:font-regular rb:pl-1">{t('userMemory.member_count')}</span>
<span className="rb:font-medium">{(selectedNode as RawCommunityNode).properties.member_count}{t('userMemory.member_count_desc')}</span>
</Flex>
<div className="rb:font-medium rb:text-[#212332] rb:text-[16px] rb:leading-5.5 rb:pl-1">
{(selectedNode as RawCommunityNode).properties.name || selectedNode.id}
</div>
{(selectedNode as RawCommunityNode).properties.summary && <>
<div className="rb:mt-3 rb:font-medium rb:leading-5 rb:pl-1">{t('userMemory.summary')}</div>
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:px-3 rb:py-2.5 rb:mt-2">
{(selectedNode as RawCommunityNode).properties.summary}
</div>
</>}
<Flex align="center" justify="space-between" className="rb:mt-5!">
<span className="rb:text-[#5B6167] rb:font-regular rb:pl-1">{t('userMemory.member_count')}</span>
<span className="rb:font-medium">{(selectedNode as RawCommunityNode).properties.member_count}{t('userMemory.member_count_desc')}</span>
</Flex>
<Divider className='rb:my-2.5!' />
<div className="rb:font-medium rb:leading-5 rb:pl-1">{t('userMemory.core_entities')}</div>
<ul className="rb:list-disc rb:pl-4 rb:text-[#5B6167] rb:mt-2">
{(selectedNode as RawCommunityNode).properties.core_entities.map((entity, index) => <li key={index}>{entity}</li>)}
</ul>
</div>
{(selectedNode as RawCommunityNode).properties.core_entities && <>
<Divider className='rb:my-2.5!' />
<div className="rb:font-medium rb:leading-5 rb:pl-1">{t('userMemory.core_entities')}</div>
<ul className="rb:list-disc rb:pl-4 rb:text-[#5B6167] rb:mt-2">
{(selectedNode as RawCommunityNode).properties.core_entities?.map((entity, index) => <li key={index}>{entity}</li>)}
</ul>
</>}
</div>
: <>
{(selectedNode as Node).name &&
<div className="rb:font-medium rb:text-[16px] rb:text-[#212332] rb:leading-5.5 rb:mb-3">

View File

@@ -4,12 +4,14 @@
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 15:10:17
*/
import { type FC, useEffect, useState, useMemo } from 'react'
import { type FC, useEffect, useState, useMemo, useRef } from 'react'
import clsx from 'clsx'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Row, Col, Skeleton, Button, Divider, Tooltip, Flex } from 'antd'
import InfiniteScroll from 'react-infinite-scroll-component'
import RbCard from '@/components/RbCard/Card'
import {
getConversations,
@@ -61,6 +63,8 @@ const WorkingDetail: FC = () => {
const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<Conversation[]>([])
const [hasMore, setHasMore] = useState<boolean>(true)
const pageRef = useRef<number>(1)
const [messagesLoading, setMessagesLoading] = useState<boolean>(false)
const [messages, setMessages] = useState<ChatItem[]>([])
const [detailLoading, setDetailLoading] = useState<boolean>(false)
@@ -80,17 +84,30 @@ const WorkingDetail: FC = () => {
setSelected(null)
setDetail(null)
setData([])
getConversations(id).then((res) => {
const response = res as Conversation[]
setData(response)
setSelected(response[0] || null)
setHasMore(true)
pageRef.current = 1
getConversations(id, 1).then((res) => {
const response = res as { items: Conversation[], page: { hasnext: boolean } }
setData(response.items)
setSelected(response.items[0] || null)
setHasMore(response.page.hasnext)
})
.finally(() => {
setLoading(false)
})
}
/* Load messages and AI insight whenever the selected conversation changes. */
const loadMore = () => {
if (!id) return
const nextPage = pageRef.current + 1
getConversations(id, nextPage).then((res) => {
const response = res as {items: Conversation[], page: { hasnext: boolean }}
setData(prev => [...prev, ...response.items])
pageRef.current = nextPage
setHasMore(response.page.hasnext)
})
}
useEffect(() => {
if (!id || !selected || !selected.id) return
getDetail(selected.id)
@@ -138,16 +155,16 @@ const WorkingDetail: FC = () => {
: data.length === 0
? <Empty />
:(
<Row gutter={16} className="rb:h-full">
<Col flex='360px' className="rb:h-full">
<RbCard
title={t('workingDetail.conversation')}
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName='rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)]'
className="rb:h-full!"
>
<Flex gap={8} vertical>
<Row gutter={16}>
<Col span={5}>
<div id="conversation-list" className="rb:h-[calc(100vh-76px)]! rb:border-r rb:border-[#EAECEE] rb:py-3 rb:px-4 rb:overflow-y-auto">
<InfiniteScroll
dataLength={data.length}
next={loadMore}
hasMore={hasMore}
loader={null}
scrollableTarget="conversation-list"
>
{data.map(item => (
<Flex
key={item.id}
@@ -166,8 +183,8 @@ const WorkingDetail: FC = () => {
</Tooltip>
</Flex>
))}
</Flex>
</RbCard>
</InfiniteScroll>
</div>
</Col>
{selected && <>
<Col flex="auto" className="rb:h-full">

View File

@@ -151,7 +151,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
setLoading(true)
const message = msg
const files = toolbarRef.current?.getFiles() || []
const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
setChatList(prev => [...prev, {
role: 'user',
content: message,

View File

@@ -18,8 +18,8 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
const isUserInputRef = useRef(false);
useEffect(() => {
// 监听编辑器变化,标记是否为用户输入
const removeListener = editor.registerUpdateListener(({ editorState }) => {
const removeListener = editor.registerUpdateListener(({ editorState, tags }) => {
if (tags.has('programmatic')) return;
editorState.read(() => {
const root = $getRoot();
const textContent = root.getTextContent();
@@ -107,7 +107,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
});
root.append(paragraph);
}
}, { discrete: true });
}, { discrete: true, tag: 'programmatic' });
});
}