Merge branch 'develop' into feature/workflow_import_zy
This commit is contained in:
@@ -27,12 +27,45 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
}) => {
|
||||
// Scroll container reference for controlling auto-scroll to bottom
|
||||
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
|
||||
const prevDataLengthRef = useRef(data.length);
|
||||
const isScrolledToBottomRef = useRef(true); // Track if user is scrolled to bottom
|
||||
|
||||
// Track scroll position to determine if user is at bottom
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
||||
// Consider user is at bottom if within 20px of the bottom
|
||||
isScrolledToBottomRef.current = scrollHeight - scrollTop - clientHeight < 20;
|
||||
}
|
||||
};
|
||||
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
// Initial check
|
||||
handleScroll();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to bottom when data changes to show latest messages
|
||||
// When data array length remains unchanged, if data is updated and user manually scrolled up, don't auto-scroll to bottom
|
||||
// When data array length changes, auto-scroll to bottom
|
||||
// If already scrolled to bottom, will auto-scroll to bottom
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
// Auto-scroll if data length changed OR user is currently at bottom
|
||||
if (data.length !== prevDataLengthRef.current || isScrolledToBottomRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
prevDataLengthRef.current = data.length;
|
||||
}
|
||||
}, 0);
|
||||
}, [data])
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:12:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 14:06:28
|
||||
* @Last Modified time: 2026-02-28 17:28:41
|
||||
*/
|
||||
/**
|
||||
* BasicLayout Component
|
||||
* BasicAuthLayout Component
|
||||
*
|
||||
* A minimal layout wrapper that provides:
|
||||
* - User information initialization
|
||||
@@ -26,12 +26,12 @@ import { useUser } from '@/store/user';
|
||||
* Basic layout component for pages without navigation UI.
|
||||
* Fetches user info and storage type on mount, then renders child routes.
|
||||
*/
|
||||
const BasicLayout: FC = () => {
|
||||
const BasicAuthLayout: FC = () => {
|
||||
const { getUserInfo } = useUser();
|
||||
|
||||
// Fetch user information and storage type on component mount
|
||||
useEffect(() => {
|
||||
getUserInfo();
|
||||
getUserInfo(undefined, true); // Pass true to skip navigation jump
|
||||
}, [getUserInfo]);
|
||||
|
||||
return (
|
||||
@@ -42,4 +42,4 @@ const BasicLayout: FC = () => {
|
||||
)
|
||||
};
|
||||
|
||||
export default BasicLayout;
|
||||
export default BasicAuthLayout;
|
||||
@@ -20,6 +20,7 @@
|
||||
import { type FC, type Key, type ReactNode, useEffect } from 'react';
|
||||
import { type RadioGroupProps } from 'antd';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/** Radio card option interface */
|
||||
interface RadioCardOption {
|
||||
@@ -33,6 +34,8 @@ interface RadioCardOption {
|
||||
icon?: string;
|
||||
/** Whether the option is disabled */
|
||||
disabled?: boolean;
|
||||
/** Whether the option is recommended */
|
||||
recommend?: boolean;
|
||||
/** Additional properties */
|
||||
[key: string]: string | number | boolean | undefined | null | Key;
|
||||
}
|
||||
@@ -63,6 +66,7 @@ const RadioGroupCard: FC<RadioCardProps> = ({
|
||||
allowClear = true,
|
||||
block = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
/** Listen to value changes and trigger side effects via onValueChange callback */
|
||||
useEffect(() => {
|
||||
if (onValueChange) {
|
||||
@@ -91,12 +95,13 @@ const RadioGroupCard: FC<RadioCardProps> = ({
|
||||
})}>
|
||||
{/* Render each option as a selectable card */}
|
||||
{options.map(option => (
|
||||
<div key={String(option.value)} className={clsx("rb:border rb:rounded-lg rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
|
||||
<div key={String(option.value)} className={clsx("rb:relative rb:border rb:rounded-lg rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
|
||||
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF]': option.value === value,
|
||||
'rb:border-[#EBEBEB] rb:bg-[#ffffff]': option.value !== value,
|
||||
'rb:opacity-[0.75]': option.disabled,
|
||||
'rb:flex rb:items-center rb:text-left rb:gap-4': block,
|
||||
})} onClick={() => handleChange(option)}>
|
||||
{option.recommend && <div className="rb:absolute rb:right-0 rb:top-0 rb:bg-[#FF5D34] rb:rounded-[0px_7px_0px_8px] rb:text-[12px] rb:text-white rb:font-regular rb:leading-4 rb:p-[4px_8px]">{t('common.recommend')}</div>}
|
||||
{/* Use custom render or default card layout */}
|
||||
{itemRender ? itemRender(option) : (
|
||||
<>
|
||||
|
||||
@@ -41,6 +41,8 @@ interface SearchInputProps {
|
||||
className?: string;
|
||||
/** Input size */
|
||||
size?: InputProps['size']
|
||||
/** Maximum length of the input value */
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
/** Search input component with debounce and throttle support */
|
||||
|
||||
@@ -452,6 +452,9 @@ export const en = {
|
||||
nextStep: 'Next Step',
|
||||
prevStep: 'Previous Step',
|
||||
exportSuccess: 'Export successful',
|
||||
recommend: 'Recommend',
|
||||
logoTip: `Supported image formats: JPG, PNG \n Suggested size: square ratio \n Maximum size: ≤ 2MB`,
|
||||
imageSquareRequired: 'Please upload a square image',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -541,7 +544,8 @@ export const en = {
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
bedrock: "Bedrock",
|
||||
nameInvalid: 'Model name can only contain letters, numbers, underscores and spaces, cannot be empty or pure whitespace',
|
||||
},
|
||||
modelNew: {
|
||||
group: 'Model Group',
|
||||
@@ -1642,6 +1646,10 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
scene_type_distribution: 'Scene Type Distribution',
|
||||
general_type_distribution: 'General Type Distribution',
|
||||
unmatched: 'Unmatched',
|
||||
disagreementCase: 'Disagreement Case',
|
||||
Pruned: 'Pruned',
|
||||
pruning: 'Pruning',
|
||||
pruning_desc: 'Text pruning {{count}} fragments'
|
||||
},
|
||||
memoryConversation: {
|
||||
searchPlaceholder: 'Enter user ID...',
|
||||
|
||||
@@ -1031,6 +1031,9 @@ export const zh = {
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
exportSuccess: '导出成功',
|
||||
recommend: '推荐',
|
||||
logoTip: `支持图片格式(JPG、PNG)\n 尺寸:正方形比例 \n 文件大小限制:≤ 2MB`,
|
||||
imageSquareRequired: '请上传正方形比例图片',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: '搜索模型…',
|
||||
@@ -1178,7 +1181,8 @@ export const zh = {
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
bedrock: "Bedrock",
|
||||
nameInvalid: '模型名称只能包含字母、数字、下划线和空格, 不能为空或纯空格',
|
||||
},
|
||||
timezones: {
|
||||
'Asia/Shanghai': '中国标准时间 (UTC+8)',
|
||||
@@ -1639,6 +1643,10 @@ export const zh = {
|
||||
scene_type_distribution: '场景类型',
|
||||
general_type_distribution: '通用类型',
|
||||
unmatched: '未匹配',
|
||||
disagreementCase: '不一致案例',
|
||||
Pruned: '已剪枝',
|
||||
pruning: '剪枝',
|
||||
pruning_desc: '文本剪枝{{count}}个片段'
|
||||
},
|
||||
memoryConversation: {
|
||||
chatEmpty:'有什么我可以帮您的吗?',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 16:33:54
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 18:30:10
|
||||
* @Last Modified time: 2026-02-28 17:21:20
|
||||
*/
|
||||
/**
|
||||
* User Store
|
||||
@@ -44,7 +44,7 @@ export interface UserState {
|
||||
/** Update login information */
|
||||
updateLoginInfo: (values: LoginInfo) => void;
|
||||
/** Get user information */
|
||||
getUserInfo: (flag?: boolean) => void;
|
||||
getUserInfo: (flag?: boolean, notNeedJump?: boolean) => void;
|
||||
/** Clear user information */
|
||||
clearUserInfo: () => void;
|
||||
/** Logout user */
|
||||
@@ -73,13 +73,13 @@ export const useUser = create<UserState>((set, get) => ({
|
||||
cookieUtils.set('refreshToken', values.refresh_token);
|
||||
set({ loginInfo: values });
|
||||
},
|
||||
getUserInfo: async (flag?: boolean) => {
|
||||
getUserInfo: async (flag?: boolean, notNeedJump?: boolean) => {
|
||||
if (!cookieUtils.get('authToken')) {
|
||||
return
|
||||
}
|
||||
const { checkJump } = get()
|
||||
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
|
||||
if (localUser.id) {
|
||||
if (localUser.id && !notNeedJump) {
|
||||
checkJump()
|
||||
return
|
||||
}
|
||||
|
||||
50
web/src/utils/validator.ts
Normal file
50
web/src/utils/validator.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-02 13:46:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-02 14:38:33
|
||||
*/
|
||||
/**
|
||||
* Form validation utilities
|
||||
*/
|
||||
|
||||
interface UploadFile {
|
||||
originFileObj: Blob;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if uploaded image is square (width === height)
|
||||
* @param errorMessage - Error message to display when validation fails
|
||||
* @returns Ant Design form validator
|
||||
*/
|
||||
export const validateSquareImage = (errorMessage: string = 'Image must be square') => {
|
||||
return (_: unknown, value: UploadFile | UploadFile[] | undefined) => {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const file = Array.isArray(value) ? value[0] : value;
|
||||
|
||||
if (file?.originFileObj) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (img.width === img.height) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(errorMessage));
|
||||
}
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image'));
|
||||
img.src = URL.createObjectURL(file.originFileObj);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
};
|
||||
|
||||
// - Cannot be empty or pure whitespace
|
||||
// - Cannot start with a space
|
||||
export const stringRegExp = /^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fa5\s]*$/
|
||||
@@ -12,6 +12,7 @@ import dayjs from 'dayjs'
|
||||
import type { ApiKey, ApiKeyModalRef } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createApiKey, updateApiKey } from '@/api/apiKey';
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -78,7 +79,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const { memory, rag, expires_at, ...rest } = values
|
||||
let scopes = []
|
||||
const scopes = []
|
||||
|
||||
if (memory) {
|
||||
scopes.push('memory')
|
||||
@@ -130,7 +131,11 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('apiKey.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
@@ -138,6 +143,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('apiKey.description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} rows={3} />
|
||||
</FormItem>
|
||||
|
||||
@@ -169,8 +169,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
getApplicationConfig(id as string).then(res => {
|
||||
const response = res as Config
|
||||
const { skills, variables } = response
|
||||
let allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
|
||||
let allTools = Array.isArray(response.tools) ? response.tools : []
|
||||
const allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
|
||||
const allTools = Array.isArray(response.tools) ? response.tools : []
|
||||
const memoryContent = response.memory?.memory_config_id
|
||||
const parsedMemoryContent = memoryContent === null || memoryContent === ''
|
||||
? undefined
|
||||
@@ -431,7 +431,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form.Item name="system_prompt" className="rb:mb-0!">
|
||||
<Form.Item
|
||||
name="system_prompt"
|
||||
className="rb:mb-0!"
|
||||
rules={[{ max: 10000 }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('application.promptPlaceholder')}
|
||||
styles={{
|
||||
|
||||
@@ -29,7 +29,7 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
|
||||
const { t } = useTranslation();
|
||||
const activeMethods = ['POST'];
|
||||
const { message, modal } = App.useApp()
|
||||
const copyContent = window.location.origin + '/v1/chat'
|
||||
const copyContent = window.location.origin + '/v1/app/chat'
|
||||
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
|
||||
const apiKeyConfigModalRef = useRef<ApiKeyConfigModalRef>(null);
|
||||
const [apiKeyList, setApiKeyList] = useState<ApiKey[]>([])
|
||||
|
||||
@@ -21,6 +21,7 @@ import WorkflowIcon from '@/assets/images/application/workflow.svg'
|
||||
import type { ApplicationModalData, ApplicationModalRef, Application } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { addApplication, updateApplication } from '@/api/application'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -131,13 +132,18 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('application.applicationName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('application.description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
@@ -152,7 +152,11 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
|
||||
<FormItem
|
||||
name="email"
|
||||
label={t('member.email')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ type: 'email' },
|
||||
{ max: 100 },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enterPlaceholder', { title: t('member.email') })} disabled={!!editingUser} />
|
||||
</FormItem>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:30:11
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 21:04:14
|
||||
* @Last Modified time: 2026-03-02 11:41:12
|
||||
*/
|
||||
/**
|
||||
* Result Component
|
||||
@@ -91,7 +91,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
setDeduplication({...initObj} as ModuleItem)
|
||||
setTestResult({} as TestResult)
|
||||
const handleStreamMessage = (list: SSEMessage[]) => {
|
||||
|
||||
|
||||
list.forEach((data: AnyObject) => {
|
||||
switch(data.event) {
|
||||
case 'text_preprocessing': // Start text preprocessing
|
||||
@@ -104,7 +104,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
case 'text_preprocessing_result': // Text preprocessing in progress
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
data: [...prev.data, data.data?.deleted_messages ? { deleted_messages: data.data?.deleted_messages } : data.data?.data],
|
||||
}))
|
||||
break
|
||||
case 'text_preprocessing_complete': // Text preprocessing complete
|
||||
@@ -193,9 +193,9 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
||||
custom_text: runForm.getFieldValue('custom_text')
|
||||
}, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
}
|
||||
const completedNum = [textPreprocessing, knowledgeExtraction, creatingNodesEdges, deduplication].filter(item => item.status === 'completed').length
|
||||
const deduplicationData = groupDataByType(deduplication.data, 'result_type')
|
||||
@@ -251,10 +251,10 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
</div>
|
||||
</>
|
||||
: !testResult || Object.keys(testResult).length === 0
|
||||
? <RbAlert color="orange" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.warning')}
|
||||
</RbAlert>
|
||||
: <RbAlert color="green" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
? <RbAlert color="orange" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.warning')}
|
||||
</RbAlert>
|
||||
: <RbAlert color="green" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.success')}
|
||||
</RbAlert>
|
||||
}
|
||||
@@ -266,15 +266,28 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{textPreprocessing.data.map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
<Markdown content={'-' + t('memoryExtractionEngine.fragment') + vo.chunk_index + ': ' + (vo.content.startsWith('\n') ? vo.content : '\n' + vo.content)} />
|
||||
</div>
|
||||
))}
|
||||
{textPreprocessing.data.map((vo, index) => {
|
||||
if (vo.deleted_messages) {
|
||||
return <div key={index} className="rb:mb-3 rb:pb-1 rb:border-b rb:border-b-[#EBEBEB]">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:mb-2">{t('memoryExtractionEngine.Pruned')}</div>
|
||||
{vo.deleted_messages.map((msg: any, idx: number) => (
|
||||
<div key={idx} className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
<Markdown content={'-' + t('memoryExtractionEngine.pruning') + (idx + 1) + ': ' + msg.content} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
<Markdown content={'-' + t('memoryExtractionEngine.fragment') + vo.chunk_index + ': ' + (vo.content.startsWith('\n') ? vo.content : '\n' + vo.content)} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{formatTime(textPreprocessing)}
|
||||
{textPreprocessing.result &&
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.text_preprocessing_desc', { count: textPreprocessing.result.total_chunks })},
|
||||
{t('memoryExtractionEngine.pruning_desc', { count: textPreprocessing.result.pruning.deleted_count || 0 })},
|
||||
{t('memoryExtractionEngine.text_preprocessing_desc', { count: textPreprocessing.result.total_chunks })},
|
||||
{t('memoryExtractionEngine.chunkerStrategy')}: {t(`memoryExtractionEngine.${lowercaseFirst(textPreprocessing.result.chunker_strategy)}`)}
|
||||
</RbAlert>
|
||||
}
|
||||
@@ -286,7 +299,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{knowledgeExtraction.data.map((vo, index) =>
|
||||
{knowledgeExtraction.data.map((vo, index) =>
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{vo.statement}</div>
|
||||
)}
|
||||
{formatTime(knowledgeExtraction)}
|
||||
@@ -345,31 +358,30 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
{Object.keys(resultObj).map((key, index) => {
|
||||
const keys = (resultObj as Record<string, string>)[key].split('.')
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="rb:text-[24px] rb:leading-7.5 rb:font-extrabold">{(testResult?.[keys[0] as keyof TestResult] as any)?.[keys[1]]}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{t(`memoryExtractionEngine.${key}`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#369F21] rb:leading-3.5 rb:font-regular">
|
||||
{}
|
||||
{key === 'extractTheNumberOfEntities' && testResult.dedup
|
||||
? t(`memoryExtractionEngine.${key}Desc`, {
|
||||
num: testResult.dedup.total_merged_count,
|
||||
exact: testResult.dedup.breakdown.exact,
|
||||
fuzzy: testResult.dedup.breakdown.fuzzy,
|
||||
llm: testResult.dedup.breakdown.llm,
|
||||
})
|
||||
: key === 'numberOfEntityDisambiguation' && testResult.disambiguation
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.disambiguation.effects?.length, block_count: testResult.disambiguation.block_count })
|
||||
: key === 'numberOfRelationalTriples' && testResult.triplets
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.triplets.count })
|
||||
:t(`memoryExtractionEngine.${key}Desc`)
|
||||
}
|
||||
<div key={index}>
|
||||
<div className="rb:text-[24px] rb:leading-7.5 rb:font-extrabold">{(testResult?.[keys[0] as keyof TestResult] as any)?.[keys[1]]}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{t(`memoryExtractionEngine.${key}`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#369F21] rb:leading-3.5 rb:font-regular">
|
||||
{key === 'extractTheNumberOfEntities' && testResult.dedup
|
||||
? t(`memoryExtractionEngine.${key}Desc`, {
|
||||
num: testResult.dedup.total_merged_count,
|
||||
exact: testResult.dedup.breakdown.exact,
|
||||
fuzzy: testResult.dedup.breakdown.fuzzy,
|
||||
llm: testResult.dedup.breakdown.llm,
|
||||
})
|
||||
: key === 'numberOfEntityDisambiguation' && testResult.disambiguation
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.disambiguation.effects?.length, block_count: testResult.disambiguation.block_count })
|
||||
: key === 'numberOfRelationalTriples' && testResult.triplets
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.triplets.count })
|
||||
:t(`memoryExtractionEngine.${key}Desc`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
)})}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
|
||||
{testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.entityDeduplicationImpact')}
|
||||
@@ -388,7 +400,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
|
||||
{testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.theEffectOfEntityDisambiguationLLMDriven')}
|
||||
@@ -399,7 +411,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
<div key={index} className={clsx("rb:text-[12px] rb:text-[#5B6167] rb:leading-4", {
|
||||
'rb:mt-4': index > 0,
|
||||
})}>
|
||||
<div className="rb:font-medium rb:mb-2">Disagreement Case {index +1}:</div>
|
||||
<div className="rb:font-medium rb:mb-2">{t('memoryExtractionEngine.disagreementCase')} {index +1}:</div>
|
||||
-{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → <span className="rb:text-[#369F21]">{item.result}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -409,7 +421,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
|
||||
{testResult?.core_entities && testResult?.core_entities.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.coreEntitiesAfterDedup')}
|
||||
@@ -433,7 +445,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
|
||||
{testResult?.triplet_samples && testResult?.triplet_samples.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.extractRelationalTriples')}
|
||||
|
||||
@@ -18,6 +18,7 @@ import RbModal from '@/components/RbModal'
|
||||
import { createMemoryConfig, updateMemoryConfig } from '@/api/memory'
|
||||
import { getOntologyScenesSimpleUrl } from '@/api/ontology'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -110,7 +111,11 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
|
||||
<FormItem
|
||||
name="config_name"
|
||||
label={t('memory.configurationName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
@@ -118,6 +123,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
|
||||
<FormItem
|
||||
name="config_desc"
|
||||
label={t('memory.desc')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:50:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:50:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-27 10:20:51
|
||||
*/
|
||||
/**
|
||||
* Model List View
|
||||
@@ -21,7 +21,7 @@ import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import KeyConfigModal from './components/KeyConfigModal'
|
||||
import ModelListDetail from './components/ModelListDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
import { getListLogoUrl } from './utils'
|
||||
|
||||
/**
|
||||
* Model list component
|
||||
@@ -70,7 +70,7 @@ const ModelList = forwardRef<BaseRef, { query: any; handleEdit: (vo?: ModelListI
|
||||
<RbCard
|
||||
key={item.provider}
|
||||
title={t(`modelNew.${item.provider}`)}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatarUrl={getListLogoUrl(item.provider, item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.provider[0].toUpperCase()}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:49:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 17:24:05
|
||||
*/
|
||||
/**
|
||||
* Custom Model Modal
|
||||
@@ -20,6 +20,7 @@ import CustomSelect from '@/components/CustomSelect'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
import { validateSquareImage, stringRegExp } from '@/utils/validator'
|
||||
|
||||
/**
|
||||
* Custom model modal component
|
||||
@@ -50,7 +51,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
logo: model.logo && model.logo.startsWith('http') ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
@@ -65,7 +66,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
const res = isEdit ? updateCustomModel(model.id, rest) : addCustomModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh && refresh(isEdit)
|
||||
refresh?.(isEdit)
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
@@ -79,7 +80,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { logo, ...rest } = values;
|
||||
let formData: CustomModelForm = {
|
||||
const formData: CustomModelForm = {
|
||||
...rest
|
||||
}
|
||||
|
||||
@@ -125,14 +126,22 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') },
|
||||
{ validator: validateSquareImage(t('common.imageSquareRequired')) }
|
||||
]}
|
||||
extra={t('common.logoTip')?.split('\n').map((vo, index) => <div key={index}>{vo}</div>)}
|
||||
>
|
||||
<UploadImages />
|
||||
<UploadImages fileSize={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
@@ -166,6 +175,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:49:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-02 12:23:13
|
||||
*/
|
||||
/**
|
||||
* Group Model Modal
|
||||
@@ -21,6 +21,7 @@ import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/mod
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import ModelImplement from './ModelImplement'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
import { validateSquareImage, stringRegExp } from '@/utils/validator'
|
||||
|
||||
/**
|
||||
* Group model modal component
|
||||
@@ -133,15 +134,26 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') },
|
||||
{ validator: validateSquareImage(t('common.imageSquareRequired')) }
|
||||
]}
|
||||
extra={t('common.logoTip')?.split('\n').map((vo, index) => <div key={index}>{vo}</div>)}
|
||||
>
|
||||
<UploadImages />
|
||||
<UploadImages
|
||||
fileSize={2}
|
||||
fileType={['png', 'jpg']}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
@@ -165,6 +177,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -121,6 +121,7 @@ const tabKeys = ['group', 'list', 'square']
|
||||
{activeTab !== 'list' &&
|
||||
<Form.Item name="search" noStyle>
|
||||
<SearchInput
|
||||
maxLength={50}
|
||||
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
|
||||
className="rb:w-70!"
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:50:22
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:50:22
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-27 10:22:46
|
||||
*/
|
||||
/**
|
||||
* Utility functions for Model Management
|
||||
@@ -40,5 +40,26 @@ export const getLogoUrl = (logo?: string) => {
|
||||
return logo
|
||||
}
|
||||
|
||||
return ICONS[logo as keyof typeof ICONS] || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo URL from provider name or URL
|
||||
* @param provider - Provider name
|
||||
* @param logo - Provider name or logo URL
|
||||
* @returns Logo URL or undefined
|
||||
*/
|
||||
export const getListLogoUrl = (provider?: string, logo?: string) => {
|
||||
let url = ICONS[provider as keyof typeof ICONS]
|
||||
|
||||
if (url) return url
|
||||
|
||||
if (!logo) {
|
||||
return undefined
|
||||
}
|
||||
if (logo.startsWith('http')) {
|
||||
return logo
|
||||
}
|
||||
|
||||
return ICONS[logo as keyof typeof ICONS] || undefined
|
||||
}
|
||||
@@ -182,7 +182,10 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
|
||||
<FormItem
|
||||
name="scenario"
|
||||
label={t('ontology.scenario')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 2000 },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.scenarioPlaceholder')} />
|
||||
</FormItem>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import type { AddClassItem, OntologyClassModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createOntologyClass } from '@/api/ontology'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -105,7 +106,11 @@ const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalP
|
||||
<FormItem
|
||||
name="class_name"
|
||||
label={t('ontology.class_name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
@@ -113,6 +118,7 @@ const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalP
|
||||
<FormItem
|
||||
name="class_description"
|
||||
label={t('ontology.class_description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.classDescriptionPlaceholder')} />
|
||||
</FormItem>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import type { OntologyItem, OntologyModalData, OntologyModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { createOntologyScene, updateOntologyScene } from '@/api/ontology'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -109,7 +110,11 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
<FormItem
|
||||
name="scene_name"
|
||||
label={t('ontology.scene_name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
@@ -117,6 +122,7 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
<FormItem
|
||||
name="scene_description"
|
||||
label={t('ontology.scene_description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.descriptionPlaceholder')} />
|
||||
</FormItem>
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { AiPromptModalRef } from '@/views/ApplicationConfig/types'
|
||||
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
|
||||
import type { SkillFormData } from '../types'
|
||||
import { getSkillDetail, createSkill, updateSkill } from '@/api/skill'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
/**
|
||||
* Skill Configuration Page Component
|
||||
@@ -110,7 +111,7 @@ const SkillConfig: FC = () => {
|
||||
// Format tools data for API
|
||||
const formData = {
|
||||
...rest,
|
||||
tools: tools?.map((item: any) => ({
|
||||
tools: tools?.map((item) => ({
|
||||
tool_id: item.tool_id,
|
||||
operation: item.operation
|
||||
}))
|
||||
@@ -144,13 +145,18 @@ const SkillConfig: FC = () => {
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('skills.name')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('skills.description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('skills.descriptionPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface SkillFormData {
|
||||
tools: Array<{
|
||||
/** Tool identifier */
|
||||
tool_id: string;
|
||||
/** Tool operation/action */
|
||||
operation?: string;
|
||||
}>;
|
||||
/** Skill configuration settings */
|
||||
config: {
|
||||
|
||||
@@ -23,6 +23,7 @@ import UploadImages from '@/components/Upload/UploadImages'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
import ragIcon from '@/assets/images/space/rag.png'
|
||||
import neo4jIcon from '@/assets/images/space/neo4j.png'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -34,8 +35,8 @@ interface SpaceModalProps {
|
||||
}
|
||||
/** Storage types */
|
||||
const types: StorageType[] = [
|
||||
'rag',
|
||||
'neo4j',
|
||||
'rag',
|
||||
]
|
||||
/** Type icons mapping */
|
||||
const typeIcons: Record<StorageType, string> = {
|
||||
@@ -91,7 +92,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
setCurrentStep(1)
|
||||
} else {
|
||||
const { icon, ...rest } = values
|
||||
let formData: SpaceModalData = {
|
||||
const formData: SpaceModalData = {
|
||||
...rest
|
||||
}
|
||||
if (icon?.response?.data.file_id) {
|
||||
@@ -154,6 +155,9 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
storage_type: types[0],
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="icon"
|
||||
@@ -161,14 +165,19 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
valuePropName="fileList"
|
||||
hidden={currentStep === 1}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.spaceIcon') }) }]}
|
||||
extra={t('common.logoTip')?.split('\n').map((vo, index) => <div key={index}>{vo}</div>)}
|
||||
>
|
||||
<UploadImages />
|
||||
<UploadImages fileSize={2} />
|
||||
</Form.Item>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('space.spaceName')}
|
||||
hidden={currentStep === 1}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('space.spaceName') }) }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.inputPlaceholder', { title: t('space.spaceName') }) },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('space.spaceName') })} />
|
||||
</FormItem>
|
||||
@@ -183,7 +192,8 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
value: type,
|
||||
label: t(`space.${type}`),
|
||||
labelDesc: t(`space.${type}Desc`),
|
||||
icon: typeIcons[type]
|
||||
icon: typeIcons[type],
|
||||
recommend: type === 'neo4j',
|
||||
}))}
|
||||
block={true}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-03 10:12:33
|
||||
*/
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { useEffect, useState, useRef, type FC } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical';
|
||||
|
||||
@@ -30,6 +30,26 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollSelectedIntoView = () => {
|
||||
if (!popupRef.current) return;
|
||||
|
||||
const selectedElement = popupRef.current.querySelector('[data-selected="true"]');
|
||||
if (!selectedElement) return;
|
||||
|
||||
const container = popupRef.current;
|
||||
const element = selectedElement as HTMLElement;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
if (elementRect.bottom > containerRect.bottom) {
|
||||
container.scrollTop += elementRect.bottom - containerRect.bottom;
|
||||
} else if (elementRect.top < containerRect.top) {
|
||||
container.scrollTop -= containerRect.top - elementRect.top;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to editor updates and show suggestions when '/' is typed
|
||||
useEffect(() => {
|
||||
@@ -140,7 +160,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
||||
};
|
||||
|
||||
// Group suggestions by node ID
|
||||
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, suggestion) => {
|
||||
const { nodeData } = suggestion
|
||||
const nodeId = nodeData.id as string;
|
||||
if (!groups[nodeId]) {
|
||||
@@ -190,7 +210,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
||||
while (nextIndex < allOptions.length && allOptions[nextIndex].disabled) {
|
||||
nextIndex++;
|
||||
}
|
||||
return nextIndex >= allOptions.length ? prev : nextIndex;
|
||||
const newIndex = nextIndex >= allOptions.length ? prev : nextIndex;
|
||||
setTimeout(() => scrollSelectedIntoView(), 0);
|
||||
return newIndex;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -210,7 +232,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
||||
while (prevIndex >= 0 && allOptions[prevIndex].disabled) {
|
||||
prevIndex--;
|
||||
}
|
||||
return prevIndex < 0 ? prev : prevIndex;
|
||||
const newIndex = prevIndex < 0 ? prev : prevIndex;
|
||||
setTimeout(() => scrollSelectedIntoView(), 0);
|
||||
return newIndex;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -247,6 +271,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={popupRef}
|
||||
data-autocomplete-popup="true"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{
|
||||
@@ -279,6 +304,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
data-selected={selectedIndex === globalIndex}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
||||
|
||||
Reference in New Issue
Block a user