Merge branch 'develop' into feature/workflow_import_zy

This commit is contained in:
yingzhao
2026-03-03 10:16:59 +08:00
committed by GitHub
77 changed files with 3159 additions and 847 deletions

View File

@@ -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])

View File

@@ -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;

View File

@@ -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) : (
<>

View File

@@ -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 */

View File

@@ -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...',

View File

@@ -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:'有什么我可以帮您的吗?',

View File

@@ -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
}

View 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]*$/

View File

@@ -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>

View File

@@ -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={{

View File

@@ -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[]>([])

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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()}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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!"
/>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -17,6 +17,8 @@ export interface SkillFormData {
tools: Array<{
/** Tool identifier */
tool_id: string;
/** Tool operation/action */
operation?: string;
}>;
/** Skill configuration settings */
config: {

View File

@@ -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}
/>

View File

@@ -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',