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

@@ -52,6 +52,10 @@ export const getKnowledgeBaseTypeList = async (): Promise<string[]> => {
// 如果不是数组,返回空数组
return [];
};
// 获取文件地址
export const getFileUrl = (fileId: string) => {
return `${apiPrefix}/files/${fileId}`;
};
// 知识库文档解析类型
export const getKnowledgeBaseDocumentParseTypeList = async () => {
const response = await request.get(`${apiPrefix}/knowledges/parsertype`);

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:06
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 10:48:41
* @Last Modified time: 2026-03-19 18:35:10
*/
import { request } from '@/utils/request'
import type { AxiosRequestConfig } from 'axios'
@@ -218,8 +218,8 @@ export const getExplicitMemory = (end_user_id: string) => {
export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
return request.post(`/memory/explicit-memory/details`, data)
}
export const getConversations = (end_user_id: string) => {
return request.get(`/memory/work/${end_user_id}/conversations`)
export const getConversations = (end_user_id: string, page = 1, pagesize = 20) => {
return request.get(`/memory/work/${end_user_id}/conversations`, { page, pagesize })
}
export const getConversationMessages = (end_user_id: string, conversation_id: string) => {
return request.get(`/memory/work/${end_user_id}/messages`, { conversation_id })

View File

@@ -143,15 +143,20 @@ const ChatContent: FC<ChatContentProps> = ({
}
return (
<div key={file.url || file.uid} className="rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:p-1! rb:cursor-pointer" onClick={() => handleDownload(file)}>
{(file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document')) && <div
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/word.svg')]"
></div>}
{(file.type.includes('pdf')) && <div
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf.svg')]"
></div>}
{(file.type.includes('excel') || file.type.includes('spreadsheetml.sheet') || file.type.includes('csv')) && <div
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/excel.svg')]"
></div>}
{(file.type.includes('excel') || file.type.includes('spreadsheetml.sheet') || file.type.includes('csv'))
? <div
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/excel.svg')]"
></div>
:(file.type.includes('pdf'))
? <div
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf.svg')]"
></div>
: (file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document'))
? <div
className="rb:size-10 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/word.svg')]"
></div>
: null
}
</div>
)
})}

View File

@@ -49,6 +49,7 @@ interface FormValues {
memory?: boolean;
}
const max_file_count = 1;
const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
features,
leftExtra,
@@ -86,10 +87,16 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
// Append newly uploaded file to the file list when upload is complete
const fileChange = (file?: any) => {
if (file?.status !== 'done') return
const files = [...(queryValues?.files || []), file]
form.setFieldValue('files', files)
onFilesChange?.(files)
console.log('file', file)
const lastFiles = form.getFieldValue('files') || [];
const index = lastFiles.findIndex((item: any) => item.uid === file.uid)
if (index > -1) {
lastFiles[index] = file
} else {
lastFiles.push(file)
}
form.setFieldValue('files', [...lastFiles])
onFilesChange?.([...lastFiles])
}
// Append recorded audio file to the file list and notify parent
@@ -129,8 +136,8 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
key: 'url',
label: t('memoryConversation.addRemoteFile'),
onClick: () => {
if ((queryValues?.files?.length || 0) >= file_upload.max_file_count) {
messageApi.warning(t('common.fileNumTip', { num: file_upload.max_file_count }))
if ((queryValues?.files?.length || 0) >= max_file_count) {
messageApi.warning(t('common.fileNumTip', { num: max_file_count }))
return
}
uploadFileListModalRef.current?.handleOpen()
@@ -146,7 +153,7 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
onChange={fileChange}
requestConfig={uploadRequestConfig}
featureConfig={file_upload}
disabled={(queryValues?.files?.length || 0) >= file_upload.max_file_count}
disabled={(queryValues?.files?.length || 0) >= max_file_count}
/>
)
})
@@ -184,7 +191,7 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
{rightExtra}
{file_upload?.audio_enabled && file_upload?.allowed_transfer_methods?.includes('local_file') &&
<AudioRecorder
disabled={(queryValues?.files?.length || 0) >= file_upload.max_file_count}
disabled={(queryValues?.files?.length || 0) >= max_file_count}
action={uploadAction}
requestConfig={uploadRequestConfig}
onRecordingComplete={handleRecordingComplete}

View File

@@ -4,7 +4,7 @@
* @Author: yujiangping
* @Date: 2026-03-16 19:01:12
* @LastEditors: yujiangping
* @LastEditTime: 2026-03-18 18:35:53
* @LastEditTime: 2026-03-20 12:12:20
*/
import { useState, useEffect, useRef, useCallback, type FC } from 'react';
import { Spin, Alert, Button, Table, InputNumber, Image } from 'antd';
@@ -309,23 +309,64 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
}
};
const [csvTruncated, setCsvTruncated] = useState(false);
const isCsvFile = () => getFileExtension() === '.csv';
// CSV 预览大小限制1MB
const CSV_PREVIEW_SIZE = 1 * 1024 * 1024;
// 最大预览行数
const MAX_PREVIEW_ROWS = 500;
const fetchFileBufferWithLimit = async (url: string, maxBytes?: number): Promise<ArrayBuffer> => {
const requestUrl = getRequestUrl(url);
const headers: Record<string, string> = {
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
};
if (maxBytes) {
headers['Range'] = `bytes=0-${maxBytes - 1}`;
}
const response = await fetch(requestUrl, {
credentials: 'include',
headers,
});
if (!response.ok && response.status !== 206) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.arrayBuffer();
};
const loadExcelFile = async () => {
setLoading(true);
setError(false);
setErrorMessage('');
setCsvTruncated(false);
try {
const arrayBuffer = await fetchFileBuffer(fileUrl);
// CSV 文件需要处理编码问题(可能是 GBK/GB2312
// CSV 文件需要处理编码问题(可能是 GBK/GB2312且大文件只取前 1MB
if (isCsvFile()) {
let arrayBuffer: ArrayBuffer;
let truncated = false;
try {
// 先尝试 Range 请求只取前 1MB
arrayBuffer = await fetchFileBufferWithLimit(fileUrl, CSV_PREVIEW_SIZE);
// 如果返回的数据刚好等于限制大小,说明可能被截断了
if (arrayBuffer.byteLength >= CSV_PREVIEW_SIZE) {
truncated = true;
}
} catch {
// Range 请求不支持时,全量获取后截断
const fullBuffer = await fetchFileBuffer(fileUrl);
if (fullBuffer.byteLength > CSV_PREVIEW_SIZE) {
arrayBuffer = fullBuffer.slice(0, CSV_PREVIEW_SIZE);
truncated = true;
} else {
arrayBuffer = fullBuffer;
}
}
let csvText: string;
// 先尝试 UTF-8 解码
const utf8Text = new TextDecoder('utf-8').decode(arrayBuffer);
// 检测是否有乱码特征(常见的 GBK 被错误解析为 UTF-8 的替换字符)
if (utf8Text.includes('\uFFFD') || /[\x80-\xff]/.test(utf8Text.slice(0, 200))) {
// 尝试 GBK 解码
try {
csvText = new TextDecoder('gbk').decode(arrayBuffer);
} catch {
@@ -334,19 +375,35 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
} else {
csvText = utf8Text;
}
// 如果被截断,去掉最后一行不完整的数据
if (truncated) {
const lastNewline = csvText.lastIndexOf('\n');
if (lastNewline > 0) {
csvText = csvText.substring(0, lastNewline);
}
}
const workbook = XLSX.read(csvText, { type: 'string' });
const sheets = workbook.SheetNames.map(sheetName => {
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
let data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
// 限制最大行数
if (data.length > MAX_PREVIEW_ROWS + 1) {
data = data.slice(0, MAX_PREVIEW_ROWS + 1); // +1 保留表头
truncated = true;
}
return { sheetName, data };
});
setCsvTruncated(truncated);
setExcelData(sheets);
setLoading(false);
return;
}
const arrayBuffer = await fetchFileBuffer(fileUrl);
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
const sheets = workbook.SheetNames.map(sheetName => {
const sheets = workbook.SheetNames.map((sheetName: string) => {
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
return { sheetName, data };
@@ -522,9 +579,14 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
)
)}
{/* Excel 预览 */}
{/* Excel/CSV 预览 */}
{isExcelFile() && !error && !loading && (
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
{csvTruncated && (
<div className="rb:mb-3 rb:px-3 rb:py-2 rb:bg-yellow-50 rb:border rb:border-yellow-200 rb:rounded rb:text-sm rb:text-yellow-700">
{MAX_PREVIEW_ROWS}
</div>
)}
{excelData.map((sheet, index) => (
<div key={index} className="rb:mb-6">
<h3 className="rb:text-lg rb:font-semibold rb:mb-3">{sheet.sheetName}</h3>
@@ -541,6 +603,7 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
scroll={{ x: 'max-content' }}
size="small"
bordered
virtual
/>
)}
</div>

View File

@@ -469,6 +469,7 @@ export const en = {
download: 'Download',
view: 'View',
updated_at: 'Updated At',
callbackUrlInvalid: 'Please enter a valid URL',
},
model: {
searchPlaceholder: 'search model…',

View File

@@ -1106,6 +1106,7 @@ export const zh = {
download: '下载',
view: '查看',
updated_at: '更新时间',
callbackUrlInvalid: '请输入有效的 URL',
},
model: {
searchPlaceholder: '搜索模型…',

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' });
});
}