Merge pull request #292 from SuanmoSuanyangTechnology/docs/web_zy

style(web): translate the comments in the src/views directory into En…
This commit is contained in:
yingzhao
2026-02-04 10:29:26 +08:00
committed by GitHub
157 changed files with 4271 additions and 1559 deletions

View File

@@ -2154,6 +2154,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
resilience: 'Resilience', resilience: 'Resilience',
suggestions: 'Personalized Suggestions', suggestions: 'Personalized Suggestions',
suggestionLoading: 'Your personalized suggestions are being generated', suggestionLoading: 'Your personalized suggestions are being generated',
item: 'item',
}, },
reflectionEngine: { reflectionEngine: {
reflectionEngineConfig: 'Reflection Engine Configuration', reflectionEngineConfig: 'Reflection Engine Configuration',

View File

@@ -2243,6 +2243,7 @@ export const zh = {
resilience: '恢复力', resilience: '恢复力',
suggestions: '个性化建议', suggestions: '个性化建议',
suggestionLoading: '您的个性化建议正在生成中', suggestionLoading: '您的个性化建议正在生成中',
item: '个',
}, },
reflectionEngine: { reflectionEngine: {
reflectionEngineConfig: '反思引擎配置', reflectionEngineConfig: '反思引擎配置',

View File

@@ -1,7 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 15:52:44
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:00:02
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Switch, Button, Tooltip } from 'antd'; import { Switch, Button, Tooltip } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ApiKey, ApiKeyModalRef } from '../types'; import type { ApiKey, ApiKeyModalRef } from '../types';
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import { getApiKey } from '@/api/apiKey'; import { getApiKey } from '@/api/apiKey';
@@ -9,16 +16,29 @@ import { formatDateTime } from '@/utils/format'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import { maskApiKeys } from '@/utils/apiKeyReplacer'; import { maskApiKeys } from '@/utils/apiKeyReplacer';
/**
* Modal component for viewing API key details
* Displays read-only information about an API key
*/
const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: string) => void }>(({ handleCopy }, ref) => { const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: string) => void }>(({ handleCopy }, ref) => {
// Hooks
const { t } = useTranslation(); const { t } = useTranslation();
// State
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [data, setData] = useState<ApiKey>({} as ApiKey) const [data, setData] = useState<ApiKey>({} as ApiKey)
// 封装取消方法,添加关闭弹窗逻辑 /**
* Close the modal
*/
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
}; };
/**
* Open modal and fetch API key details
* @param apiKey - API key item to view
*/
const handleOpen = (apiKey?: ApiKey) => { const handleOpen = (apiKey?: ApiKey) => {
if (apiKey?.id) { if (apiKey?.id) {
getApiKey(apiKey.id) getApiKey(apiKey.id)
@@ -29,7 +49,9 @@ const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: str
} }
}; };
// 暴露给父组件的方法 /**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -84,7 +106,6 @@ const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: str
</span> </span>
</div> </div>
{/* 高级设置 */}
{data.expires_at && <> {data.expires_at && <>
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.advancedSettings')}</div> <div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.advancedSettings')}</div>

View File

@@ -1,28 +1,48 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 15:52:47
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:00:01
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, Switch, App, DatePicker } from 'antd'; import { Form, Input, Switch, App, DatePicker } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'
import type { ApiKey, ApiKeyModalRef } from '../types'; import type { ApiKey, ApiKeyModalRef } from '../types';
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import dayjs from 'dayjs'
import { createApiKey, updateApiKey } from '@/api/apiKey'; import { createApiKey, updateApiKey } from '@/api/apiKey';
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Props for ApiKeyModal component
*/
interface CreateModalProps { interface CreateModalProps {
/** Callback to refresh parent list after save */
refresh: () => void; refresh: () => void;
} }
/**
* Modal component for creating or editing API keys
* Handles API key configuration including permissions and expiration
*/
const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
refresh, refresh,
}, ref) => { }, ref) => {
// Hooks
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp(); const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<ApiKey>(); const [form] = Form.useForm<ApiKey>();
// State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [editVo, setEditVo] = useState<ApiKey | null>(null); const [editVo, setEditVo] = useState<ApiKey | null>(null);
// 封装取消方法,添加关闭弹窗逻辑 /**
* Close modal and reset form state
*/
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
@@ -30,10 +50,14 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
setEditVo(null); setEditVo(null);
}; };
/**
* Open modal for creating or editing
* @param apiKey - Optional API key data for edit mode
*/
const handleOpen = (apiKey?: ApiKey) => { const handleOpen = (apiKey?: ApiKey) => {
if (apiKey?.id) { if (apiKey?.id) {
const { scopes = [], expires_at } = apiKey const { scopes = [], expires_at } = apiKey
// 编辑模式,填充表单 // Edit mode - populate form with existing data
form.setFieldsValue({ form.setFieldsValue({
name: apiKey.name, name: apiKey.name,
description: apiKey.description, description: apiKey.description,
@@ -46,7 +70,10 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /**
* Validate and submit form data
* Creates new API key or updates existing one
*/
const handleSave = async () => { const handleSave = async () => {
form.validateFields() form.validateFields()
.then((values) => { .then((values) => {
@@ -59,7 +86,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
if (rag) { if (rag) {
scopes.push('rag') scopes.push('rag')
} }
// 准备新的/更新的API Key数据 // Prepare new/updated API key data
const apiKeyData = { const apiKeyData = {
...rest, ...rest,
scopes, scopes,
@@ -78,7 +105,9 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
}) })
} }
// 暴露给父组件的方法 /**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -133,7 +162,6 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
<Switch /> <Switch />
</FormItem> </FormItem>
{/* 高级设置 */}
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.advancedSettings')}</div> <div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.advancedSettings')}</div>
<FormItem <FormItem

View File

@@ -1,8 +1,16 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 15:52:50
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 15:52:50
*/
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, App, Space } from 'antd'; import { Button, App, Space } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
import copy from 'copy-to-clipboard'
import type { ApiKey, ApiKeyModalRef } from './types'; import type { ApiKey, ApiKeyModalRef } from './types';
import ApiKeyModal from './components/ApiKeyModal'; import ApiKeyModal from './components/ApiKeyModal';
import ApiKeyDetailModal from './components/ApiKeyDetailModal'; import ApiKeyDetailModal from './components/ApiKeyDetailModal';
@@ -11,26 +19,49 @@ import { getApiKeyListUrl, deleteApiKey } from '@/api/apiKey';
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import copy from 'copy-to-clipboard'
import { maskApiKeys } from '@/utils/apiKeyReplacer'; import { maskApiKeys } from '@/utils/apiKeyReplacer';
/**
* API Key Management page component
* Manages service API keys with CRUD operations
*/
const ApiKeyManagement: React.FC = () => { const ApiKeyManagement: React.FC = () => {
// Hooks
const { t } = useTranslation(); const { t } = useTranslation();
const { modal, message } = App.useApp(); const { modal, message } = App.useApp();
// Refs
const apiKeyModalRef = useRef<ApiKeyModalRef>(null); const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
const apiKeyDetailModalRef = useRef<ApiKeyModalRef>(null) const apiKeyDetailModalRef = useRef<ApiKeyModalRef>(null)
const scrollListRef = useRef<PageScrollListRef>(null) const scrollListRef = useRef<PageScrollListRef>(null)
/**
* Refresh the API key list
*/
const refresh = () => { const refresh = () => {
scrollListRef.current?.refresh(); scrollListRef.current?.refresh();
} }
/**
* Open modal to create or edit API key
* @param item - Optional API key item for edit mode
*/
const handleEdit = (item?: ApiKey) => { const handleEdit = (item?: ApiKey) => {
apiKeyModalRef.current?.handleOpen(item); apiKeyModalRef.current?.handleOpen(item);
} }
/**
* Open modal to view API key details
* @param item - API key item to view
*/
const handleView = (item: ApiKey) => { const handleView = (item: ApiKey) => {
apiKeyDetailModalRef.current?.handleOpen(item); apiKeyDetailModalRef.current?.handleOpen(item);
} }
/**
* Delete API key with confirmation
* @param item - API key item to delete
*/
const handleDelete = (item: ApiKey) => { const handleDelete = (item: ApiKey) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.name }), title: t('common.confirmDeleteDesc', { name: item.name }),
@@ -46,6 +77,10 @@ const ApiKeyManagement: React.FC = () => {
} }
}) })
} }
/**
* Copy content to clipboard
* @param content - Content to copy
*/
const handleCopy = (content: string) => { const handleCopy = (content: string) => {
copy(content) copy(content)
message.success(t('common.copySuccess')) message.success(t('common.copySuccess'))

View File

@@ -1,39 +1,76 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 15:52:53
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 15:52:53
*/
import type { Dayjs } from 'dayjs' import type { Dayjs } from 'dayjs'
import { maskApiKeys } from '@/utils/apiKeyReplacer' import { maskApiKeys } from '@/utils/apiKeyReplacer'
/**
* API Key data structure
*/
export interface ApiKey { export interface ApiKey {
/** Unique identifier */
id: string; id: string;
/** API key name */
name: string; name: string;
/** Optional description */
description?: string; description?: string;
/** API key type */
type: 'agent' | 'multi_agent' | 'workflow' | 'service'; type: 'agent' | 'multi_agent' | 'workflow' | 'service';
scopes?: string[]; // 'memory' | 'rag' | 'app' /** Permission scopes: 'memory' | 'rag' | 'app' */
scopes?: string[];
/** The actual API key string */
api_key: string; api_key: string;
/** Whether the key is active */
is_active: boolean; is_active: boolean;
/** Whether the key has expired */
is_expired: boolean; is_expired: boolean;
/** Creation timestamp */
created_at: number; created_at: number;
/** Expiration timestamp or Dayjs object */
expires_at?: number | Dayjs; expires_at?: number | Dayjs;
/** Memory engine permission flag */
memory?: boolean; memory?: boolean;
/** RAG/Knowledge base permission flag */
rag?: boolean; rag?: boolean;
/** Last update timestamp */
updated_at: string; updated_at: string;
/** Queries per second limit */
qps_limit?: number; qps_limit?: number;
/** Daily request limit */
daily_request_limit?: number; daily_request_limit?: number;
/** Rate limit */
rate_limit?: number; rate_limit?: number;
/** Total number of requests made */
total_requests: number; total_requests: number;
/** Quota used */
quota_used: number; quota_used: number;
/** Quota limit */
quota_limit: number; quota_limit: number;
} }
/**
* Ref methods exposed by API Key modal components
*/
export interface ApiKeyModalRef { export interface ApiKeyModalRef {
/**
* Open the modal
* @param apiKey - Optional API key data for edit mode
*/
handleOpen: (apiKey?: ApiKey) => void; handleOpen: (apiKey?: ApiKey) => void;
/** Close the modal */
handleClose: () => void; handleClose: () => void;
} }
/** /**
* 获取掩码后的API密钥 * Get masked API key for display
* @param apiKey - The API key to mask
* @returns Masked API key string
*/ */
export const getMaskedApiKey = (apiKey: string): string => { export const getMaskedApiKey = (apiKey: string): string => {
return maskApiKeys(apiKey) return maskApiKeys(apiKey)

View File

@@ -1,75 +0,0 @@
import { useTranslation } from 'react-i18next';
import { type FC, useEffect, useState } from 'react';
import { Row, Col, Skeleton } from 'antd'
import CodeBlock from '@/components/Markdown/CodeBlock';
import { getMemoryApi } from '@/api/memory';
import RbCard from '@/components/RbCard/Card';
import type {
Data,
Section
} from './types';
import Empty from '@/components/Empty'
const ApiParameters: FC = () => {
const { t } = useTranslation();
const [loading, setLoading] = useState<boolean>(false)
// const [data, setData] = useState<Data | null>(null)
const [apiData, setApiData] = useState<Section[]>([])
useEffect(() => {
getApiData()
}, [])
const getApiData = () => {
setLoading(true)
getMemoryApi().then((res) => {
const resp = res as Data || {}
// setData(resp)
setApiData(resp.sections || [])
})
.finally(() => setLoading(false))
}
return (
<div className="rb:pb-[24px]">
<h1 className="rb:text-2xl rb:font-semibold rb:mb-[8px]">{t('api.pageTitle')}</h1>
<p className="rb:text-[#5B6167] rb:text-[14px] rb:mb-[24px] rb:leading-[20px]">{t('api.pageSubTitle')}</p>
{loading
? <Skeleton />
: apiData.length === 0
? <Empty />
: <Row gutter={[24, 24]}>
{apiData.map((api, index) => (
<Col span={24} key={index}>
<RbCard
title={`${index + 1}. ${api.name}`}
>
<>
<div className="rb:mb-[24px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:shadow-[inset_4px_0px_0px_0px_#155EEF] rb:p-[16px_24px] rb:text-sm">
<span className="rb:bg-[#155EEF] rb:p-[2px_8px] rb:rounded-[6px] rb:text-[#fff] rb:mr-[16px]">{api.method}</span>
{api.path}
</div>
{api.desc &&<>
<div className="rb:text-base rb:font-medium rb:mb-[8px]">{t('api.desc')}</div>
<div className="rb:mb-[24px] rb:text-sm rb:text-[#5B6167]">{api.desc}</div>
</>}
{typeof api.input === 'string' && api.input !== '无' && <>
<div className="rb:text-base rb:font-medium rb:mb-[12px] rb:mt-[24px]">{t('api.input')}</div>
<CodeBlock value={api.input} />
</>}
{typeof api.output === 'string' && api.output !== '无' && <>
<div className="rb:text-base rb:font-medium rb:mb-[12px] rb:mt-[24px]">{t('api.output')}</div>
<CodeBlock value={api.output} />
</>}
</>
</RbCard>
</Col>
))}
</Row>
}
</div>
);
};
export default ApiParameters;

View File

@@ -1,22 +0,0 @@
export interface Section {
name: string;
path: string;
method: string;
input: string;
output: string;
desc: string;
}
export interface Data {
title: string;
meta: {
search_switch: {
value: string;
desc: string;
}[];
status_code: {
code: string;
desc: string;
}[];
}
sections: Section[]
}

View File

@@ -1,8 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:21
*/
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'; import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Row, Col, Space, Form, Input, Switch, Button, App, Spin } from 'antd' import { Row, Col, Space, Form, Input, Switch, Button, App, Spin } from 'antd'
import Chat from './components/Chat' import Chat from './components/Chat'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import Card from './components/Card' import Card from './components/Card'
@@ -33,6 +40,11 @@ import AiPromptModal from './components/AiPromptModal'
import ToolList from './components/ToolList/ToolList' import ToolList from './components/ToolList/ToolList'
import ChatVariableConfigModal from './components/ChatVariableConfigModal'; import ChatVariableConfigModal from './components/ChatVariableConfigModal';
/**
* Description wrapper component
* @param desc - Description text
* @param className - Additional CSS classes
*/
const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => { const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => {
return ( return (
<div className={clsx(className, "rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}> <div className={clsx(className, "rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
@@ -40,6 +52,12 @@ const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className})
</div> </div>
) )
} }
/**
* Label wrapper component
* @param title - Label title
* @param className - Additional CSS classes
* @param children - Child elements
*/
const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}> = ({title, className, children}) => { const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}> = ({title, className, children}) => {
return ( return (
<div className={clsx(className, "rb:text-[14px] rb:font-medium rb:leading-5")}> <div className={clsx(className, "rb:text-[14px] rb:font-medium rb:leading-5")}>
@@ -48,6 +66,13 @@ const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}
</div> </div>
) )
} }
/**
* Switch wrapper component with label and description
* @param title - Switch title
* @param desc - Optional description
* @param name - Form field name
* @param needTransition - Whether to translate text
*/
const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[]; needTransition?: boolean; }> = ({ title, desc, name, needTransition = true }) => { const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[]; needTransition?: boolean; }> = ({ title, desc, name, needTransition = true }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -65,6 +90,13 @@ const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[];
</div> </div>
) )
} }
/**
* Select wrapper component with label and description
* @param title - Select title
* @param desc - Description text
* @param name - Form field name
* @param url - API URL for options
*/
const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string }> = ({ title, desc, name, url }) => { const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string }> = ({ title, desc, name, url }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -88,6 +120,10 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[],
) )
} }
/**
* Agent configuration component
* Manages single agent configuration including prompts, knowledge, memory, variables, and tools
*/
const Agent = forwardRef<AgentRef>((_props, ref) => { const Agent = forwardRef<AgentRef>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams(); const { id } = useParams();
@@ -103,7 +139,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
const [isSave, setIsSave] = useState(false) const [isSave, setIsSave] = useState(false)
const initialized = useRef(false) const initialized = useRef(false)
// 初始化完成标记 // Initialization flag
useEffect(() => { useEffect(() => {
if (data) { if (data) {
initialized.current = true initialized.current = true
@@ -121,6 +157,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
getData() getData()
}, []) }, [])
/**
* Fetch agent configuration data
*/
const getData = () => { const getData = () => {
setLoading(true) setLoading(true)
getApplicationConfig(id as string).then(res => { getApplicationConfig(id as string).then(res => {
@@ -147,6 +186,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
}) })
} }
/**
* Refresh configuration after model changes
* @param vo - Model configuration
* @param type - Source type (model or chat)
*/
const refresh = (vo: ModelConfig, type: Source) => { const refresh = (vo: ModelConfig, type: Source) => {
if (type === 'model') { if (type === 'model') {
const { default_model_config_id, ...rest } = vo const { default_model_config_id, ...rest } = vo
@@ -188,20 +232,30 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
} }
} }
/**
* Open model configuration modal
*/
const handleModelConfig = () => { const handleModelConfig = () => {
modelConfigModalRef.current?.handleOpen('model') modelConfigModalRef.current?.handleOpen('model')
} }
/**
* Clear all debugging chat sessions
*/
const handleClearDebugging = () => { const handleClearDebugging = () => {
setChatList([]) setChatList([])
} }
// 保存Agent配置 /**
* Save agent configuration
* @param flag - Whether to show success message
* @returns Promise that resolves when save is complete
*/
const handleSave = (flag = true) => { const handleSave = (flag = true) => {
if (!isSave || !data) return Promise.resolve() if (!isSave || !data) return Promise.resolve()
const { memory, knowledge_retrieval, tools, ...rest } = values const { memory, knowledge_retrieval, tools, ...rest } = values
const { knowledge_bases = [], ...knowledgeRest } = knowledge_retrieval || {} const { knowledge_bases = [], ...knowledgeRest } = knowledge_retrieval || {}
const { memory_content } = memory || {} const { memory_content } = memory || {}
// 从原数据中获取memory的其他必要属性 // Get other necessary properties of memory from original data
const originalMemory = data.memory || ({} as MemoryConfig) const originalMemory = data.memory || ({} as MemoryConfig)
const params: Config = { const params: Config = {
@@ -240,6 +294,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
}) })
}) })
} }
/**
* Fetch available models list
*/
const getModels = () => { const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true }) getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => { .then(res => {
@@ -247,6 +304,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
setModelList(response.items) setModelList(response.items)
}) })
} }
/**
* Add new model for debugging
*/
const handleAddModel = () => { const handleAddModel = () => {
modelConfigModalRef.current?.handleOpen('chat') modelConfigModalRef.current?.handleOpen('chat')
} }
@@ -268,9 +328,16 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
})) }))
const aiPromptModalRef = useRef<AiPromptModalRef>(null) const aiPromptModalRef = useRef<AiPromptModalRef>(null)
/**
* Open AI prompt generation modal
*/
const handlePrompt = () => { const handlePrompt = () => {
aiPromptModalRef.current?.handleOpen() aiPromptModalRef.current?.handleOpen()
} }
/**
* Update prompt and extract variables
* @param value - New prompt value
*/
const updatePrompt = (value: string) => { const updatePrompt = (value: string) => {
form.setFieldValue('system_prompt', value) form.setFieldValue('system_prompt', value)
const variables = value.match(/\{\{([^}]+)\}\}/g)?.map(match => match.slice(2, -2)) || [] const variables = value.match(/\{\{([^}]+)\}\}/g)?.map(match => match.slice(2, -2)) || []
@@ -285,15 +352,26 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
updateVariableList(newVariableList) updateVariableList(newVariableList)
} }
/**
* Update variable list
* @param list - New variable list
*/
const updateVariableList = (list: Variable[]) => { const updateVariableList = (list: Variable[]) => {
form.setFieldValue('variables', [...list]) form.setFieldValue('variables', [...list])
setChatVariables([...list]) setChatVariables([...list])
} }
const chatVariableConfigModalRef = useRef<ChatVariableConfigModalRef>(null) const chatVariableConfigModalRef = useRef<ChatVariableConfigModalRef>(null)
const [chatVariables, setChatVariables] = useState<Variable[]>([]) const [chatVariables, setChatVariables] = useState<Variable[]>([])
/**
* Open chat variable configuration modal
*/
const handleOpenVariableConfig = () => { const handleOpenVariableConfig = () => {
chatVariableConfigModalRef.current?.handleOpen(chatVariables) chatVariableConfigModalRef.current?.handleOpen(chatVariables)
} }
/**
* Save chat variable configuration
* @param values - Variable values
*/
const handleSaveChatVariable = (values: Variable[]) => { const handleSaveChatVariable = (values: Variable[]) => {
setChatVariables(values) setChatVariables(values)
} }
@@ -347,7 +425,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
<Knowledge /> <Knowledge />
</Form.Item> </Form.Item>
{/* 记忆配置 */} {/* Memory Configuration */}
<Card title={t('application.memoryConfiguration')}> <Card title={t('application.memoryConfiguration')}>
<Space size={24} direction='vertical' style={{ width: '100%' }}> <Space size={24} direction='vertical' style={{ width: '100%' }}>
<SwitchWrapper title="dialogueHistoricalMemory" desc="dialogueHistoricalMemoryDesc" name={['memory', 'enabled']} /> <SwitchWrapper title="dialogueHistoricalMemory" desc="dialogueHistoricalMemoryDesc" name={['memory', 'enabled']} />
@@ -364,7 +442,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
<VariableList /> <VariableList />
</Form.Item> </Form.Item>
{/* 工具配置 */} {/* Tool Configuration */}
<Form.Item name="tools"> <Form.Item name="tools">
<ToolList /> <ToolList />
</Form.Item> </Form.Item>

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:29
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:29
*/
import { type FC, useState, useRef, useEffect } from 'react'; import { type FC, useState, useRef, useEffect } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -14,6 +20,11 @@ import Tag from '@/components/Tag'
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey'; import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
import { maskApiKeys } from '@/utils/apiKeyReplacer' import { maskApiKeys } from '@/utils/apiKeyReplacer'
/**
* API configuration page component
* Manages API endpoints and API keys for the application
* @param application - Current application data
*/
const Api: FC<{ application: Application | null }> = ({ application }) => { const Api: FC<{ application: Application | null }> = ({ application }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const activeMethods = ['POST']; const activeMethods = ['POST'];
@@ -23,6 +34,10 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
const apiKeyConfigModalRef = useRef<ApiKeyConfigModalRef>(null); const apiKeyConfigModalRef = useRef<ApiKeyConfigModalRef>(null);
const [apiKeyList, setApiKeyList] = useState<ApiKey[]>([]) const [apiKeyList, setApiKeyList] = useState<ApiKey[]>([])
/**
* Copy content to clipboard
* @param content - Content to copy
*/
const handleCopy = (content: string) => { const handleCopy = (content: string) => {
copy(content) copy(content)
message.success(t('common.copySuccess')) message.success(t('common.copySuccess'))
@@ -31,6 +46,9 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
useEffect(() => { useEffect(() => {
getApiList() getApiList()
}, []) }, [])
/**
* Fetch API key list for the application
*/
const getApiList = () => { const getApiList = () => {
if (!application) { if (!application) {
return return
@@ -48,6 +66,10 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
getAllStats([...list]) getAllStats([...list])
}) })
} }
/**
* Fetch statistics for all API keys
* @param list - List of API keys
*/
const getAllStats = (list: ApiKey[]) => { const getAllStats = (list: ApiKey[]) => {
const allList: ApiKey[] = [] const allList: ApiKey[] = []
list.forEach(async item => { list.forEach(async item => {
@@ -66,12 +88,23 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
}) })
} }
/**
* Open modal to add new API key
*/
const handleAdd = () => { const handleAdd = () => {
apiKeyModalRef.current?.handleOpen() apiKeyModalRef.current?.handleOpen()
} }
/**
* Open modal to edit API key
* @param vo - API key to edit
*/
const handleEdit = (vo: ApiKey) => { const handleEdit = (vo: ApiKey) => {
apiKeyConfigModalRef.current?.handleOpen(vo) apiKeyConfigModalRef.current?.handleOpen(vo)
} }
/**
* Delete API key with confirmation
* @param vo - API key to delete
*/
const handleDelete = (vo: ApiKey) => { const handleDelete = (vo: ApiKey) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: vo.name }), title: t('common.confirmDeleteDesc', { name: vo.name }),
@@ -89,7 +122,7 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
}) })
} }
// 计算total_requests总数 // Calculate total requests across all API keys
const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0); const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0);
return ( return (
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto"> <div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
@@ -129,7 +162,7 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
} }
> >
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.apiKeySubTitle')}</div> <div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.apiKeySubTitle')}</div>
{/* 总览数据 */} {/* Overview Data */}
<Row> <Row>
<Col span={6}> <Col span={6}>
<Statistic title={t('application.apiKeyTotal')} value={apiKeyList.length} /> <Statistic title={t('application.apiKeyTotal')} value={apiKeyList.length} />
@@ -138,7 +171,7 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
<Statistic title={t('application.apiKeyRequestTotal')} value={totalRequests} /> <Statistic title={t('application.apiKeyRequestTotal')} value={totalRequests} />
</Col> </Col>
</Row> </Row>
{/* API Key 列表 */} {/* API Key List */}
{apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => ( {apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => (
<div key={item.id} className="rb:mt-4 rb:p-[10px_12px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg"> <div key={item.id} className="rb:mt-4 rb:p-[10px_12px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:flex rb:items-center rb:justify-between"> <div className="rb:flex rb:items-center rb:justify-between">

View File

@@ -1,8 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:33
*/
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Card from './components/Card'
import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd' import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd'
import Card from './components/Card'
import Tag, { type TagProps } from './components/Tag' import Tag, { type TagProps } from './components/Tag'
import CustomSelect from '@/components/CustomSelect'; import CustomSelect from '@/components/CustomSelect';
import { getMultiAgentConfig, saveMultiAgentConfig, getApplicationList } from '@/api/application'; import { getMultiAgentConfig, saveMultiAgentConfig, getApplicationList } from '@/api/application';
@@ -26,6 +33,10 @@ import type { Application } from '@/views/ApplicationManagement/types'
const tagColors = ['processing', 'warning', 'default'] const tagColors = ['processing', 'warning', 'default']
const MAX_LENGTH = 5; const MAX_LENGTH = 5;
/**
* Multi-agent cluster configuration component
* Manages multi-agent orchestration, sub-agents, and collaboration modes
*/
const Cluster = forwardRef<ClusterRef>((_props, ref) => { const Cluster = forwardRef<ClusterRef>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { message } = App.useApp() const { message } = App.useApp()
@@ -41,6 +52,11 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
}, },
]) ])
/**
* Save cluster configuration
* @param flag - Whether to show success message
* @returns Promise that resolves when save is complete
*/
const handleSave = (flag = true) => { const handleSave = (flag = true) => {
if (!data) return Promise.resolve() if (!data) return Promise.resolve()
if (!values.default_model_config_id && values.orchestration_mode === 'supervisor') { if (!values.default_model_config_id && values.orchestration_mode === 'supervisor') {
@@ -80,6 +96,9 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
getData() getData()
}, [id]) }, [id])
/**
* Fetch cluster configuration data
*/
const getData = () => { const getData = () => {
if (!id) { if (!id) {
return return
@@ -113,9 +132,17 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
} }
}) })
} }
/**
* Open sub-agent modal for add or edit
* @param agent - Optional agent data for edit mode
*/
const handleSubAgentModal = (agent?: SubAgentItem) => { const handleSubAgentModal = (agent?: SubAgentItem) => {
subAgentModalRef.current?.handleOpen(agent) subAgentModalRef.current?.handleOpen(agent)
} }
/**
* Refresh sub-agents list after add or edit
* @param agent - Agent data to add or update
*/
const refreshSubAgents = (agent: SubAgentItem) => { const refreshSubAgents = (agent: SubAgentItem) => {
const index = subAgents.findIndex(item => item.agent_id === agent.agent_id) const index = subAgents.findIndex(item => item.agent_id === agent.agent_id)
const newSubAgents = [...subAgents] const newSubAgents = [...subAgents]
@@ -130,6 +157,10 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
setSubAgents(newSubAgents) setSubAgents(newSubAgents)
} }
} }
/**
* Delete sub-agent from list
* @param agent - Agent to delete
*/
const handleDeleteSubAgent = (agent: SubAgentItem) => { const handleDeleteSubAgent = (agent: SubAgentItem) => {
setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id)) setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id))
} }
@@ -138,9 +169,16 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
})) }))
const modelConfigModalRef = useRef<ModelConfigModalRef>(null) const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
/**
* Open model configuration modal
*/
const handleEditModelConfig = () => { const handleEditModelConfig = () => {
modelConfigModalRef.current?.handleOpen('multi_agent', values.model_parameters) modelConfigModalRef.current?.handleOpen('multi_agent', values.model_parameters)
} }
/**
* Save model configuration
* @param values - Model parameters
*/
const handleSaveModelConfig = (values: Config['model_parameters']) => { const handleSaveModelConfig = (values: Config['model_parameters']) => {
form.setFieldsValue({ form.setFieldsValue({
model_parameters: values model_parameters: values

View File

@@ -1,7 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:41
*/
import { type FC, useState, useEffect, useRef } from 'react'; import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx'; import clsx from 'clsx';
import { Button, Space, Input, Form, App } from 'antd'; import { Button, Space, Input, Form, App } from 'antd';
import Tag, { type TagProps } from './components/Tag' import Tag, { type TagProps } from './components/Tag'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import { getReleaseList, rollbackRelease } from '@/api/application' import { getReleaseList, rollbackRelease } from '@/api/application'
@@ -12,12 +19,21 @@ import type { Application } from '@/views/ApplicationManagement/types'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import Markdown from '@/components/Markdown' import Markdown from '@/components/Markdown'
/**
* Tag color mapping for release versions
*/
const tagColors: Record<Release['tagKey'], TagProps['color']> = { const tagColors: Record<Release['tagKey'], TagProps['color']> = {
current: 'processing', current: 'processing',
rolledBack: 'warning', rolledBack: 'warning',
history: 'default', history: 'default',
} }
/**
* Release page component
* Manages application version releases, rollbacks, and version history
* @param data - Application data
* @param refresh - Function to refresh application data
*/
const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => { const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp() const { message } = App.useApp()
@@ -30,6 +46,9 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
getData() getData()
}, [data.id]) }, [data.id])
/**
* Fetch release list data
*/
const getData = () => { const getData = () => {
refresh() refresh()
getReleaseList(data.id).then(res => { getReleaseList(data.id).then(res => {
@@ -38,6 +57,9 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
setSelectedVersion(response?.[0]) setSelectedVersion(response?.[0])
}) })
} }
/**
* Rollback to selected version
*/
const handleRollback = () => { const handleRollback = () => {
if (!selectedVersion) return if (!selectedVersion) return
rollbackRelease(data.id, selectedVersion.version).then(() => { rollbackRelease(data.id, selectedVersion.version).then(() => {
@@ -124,7 +146,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
</div> </div>
</RbCard> </RbCard>
{/* 日志 */} {/* Logs */}
<RbCard title={t('application.changeLog')} headerType="borderless"> <RbCard title={t('application.changeLog')} headerType="borderless">
<Space size={16} direction="vertical" style={{ width: '100%' }}> <Space size={16} direction="vertical" style={{ width: '100%' }}>
{selectedVersion && ( {selectedVersion && (

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:45
*/
import { type FC, useState, useEffect } from 'react'; import { type FC, useState, useEffect } from 'react';
import { Row, Col, Flex, DatePicker } from 'antd'; import { Row, Col, Flex, DatePicker } from 'antd';
import type { Dayjs } from 'dayjs' import type { Dayjs } from 'dayjs'
@@ -10,12 +16,21 @@ import { getAppStatistics } from '@/api/application';
import LineCard from './components/LineCard' import LineCard from './components/LineCard'
import type { StatisticsData, StatisticsItem } from './types' import type { StatisticsData, StatisticsItem } from './types'
/**
* Mapping of daily statistics keys to total statistics keys
*/
const TotalObj: Record<string, keyof StatisticsData> = { const TotalObj: Record<string, keyof StatisticsData> = {
daily_conversations: 'total_conversations', daily_conversations: 'total_conversations',
daily_new_users: 'total_new_users', daily_new_users: 'total_new_users',
daily_api_calls: 'total_api_calls', daily_api_calls: 'total_api_calls',
daily_tokens: 'total_tokens', daily_tokens: 'total_tokens',
} }
/**
* Statistics page component
* Displays application usage statistics with charts and date range filtering
* @param application - Application data
*/
const Statistics: FC<{ application: Application | null }> = ({ application }) => { const Statistics: FC<{ application: Application | null }> = ({ application }) => {
const [data, setData] = useState<StatisticsData>({ const [data, setData] = useState<StatisticsData>({
daily_conversations: [], daily_conversations: [],
@@ -35,6 +50,9 @@ const Statistics: FC<{ application: Application | null }> = ({ application }) =>
useEffect(() => { useEffect(() => {
getData() getData()
}, [application, query]) }, [application, query])
/**
* Fetch statistics data
*/
const getData = () => { const getData = () => {
if (!application?.id) { if (!application?.id) {
return return
@@ -49,6 +67,10 @@ const Statistics: FC<{ application: Application | null }> = ({ application }) =>
setData(res as StatisticsData) setData(res as StatisticsData)
}) })
} }
/**
* Handle date range change
* @param date - Selected date range
*/
const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => { const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => {
if (!date || !date[0] || !date[1]) return if (!date || !date[0] || !date[1]) return
setQuery({ setQuery({

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:44
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:44
*/
/**
* AI Prompt Assistant Modal
* Provides an interactive chat interface to help users optimize their prompts using AI
* Features model selection, chat history, and variable insertion
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Button, Form, Input, App, Row, Col } from 'antd'; import { Button, Form, Input, App, Row, Col } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -19,11 +31,20 @@ import AiPromptVariableModal from './AiPromptVariableModal'
import { type SSEMessage } from '@/utils/stream' import { type SSEMessage } from '@/utils/stream'
import Editor from './Editor' import Editor from './Editor'
/**
* Component props
*/
interface AiPromptModalProps { interface AiPromptModalProps {
/** Callback to refresh prompt with optimized value */
refresh: (value: string) => void; refresh: (value: string) => void;
/** Default model to pre-select */
defaultModel: ModelListItem | null; defaultModel: ModelListItem | null;
} }
/**
* AI Prompt Assistant Modal Component
* Helps users create and optimize prompts through AI-powered conversation
*/
const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
refresh, refresh,
defaultModel, defaultModel,
@@ -42,7 +63,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
const values = Form.useWatch([], form) const values = Form.useWatch([], form)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
setLoading(false) setLoading(false)
@@ -54,6 +75,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
}) })
}; };
/** Open modal and create new prompt session */
const handleOpen = () => { const handleOpen = () => {
createPromptSessions() createPromptSessions()
.then(res => { .then(res => {
@@ -66,6 +88,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
setVisible(true); setVisible(true);
}) })
}; };
/** Send user message and get AI response */
const handleSend = () => { const handleSend = () => {
if (!promptSession) return if (!promptSession) return
if (!values.model_id) { if (!values.model_id) {
@@ -115,7 +138,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
break; break;
case 'end': case 'end':
setLoading(false) setLoading(false)
// 流结束时同步表单值 // Sync form value when stream ends
form.setFieldsValue({ current_prompt: currentPromptValueRef.current }) form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
break break
} }
@@ -134,14 +157,17 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
setLoading(false) setLoading(false)
}) })
} }
/** Copy current prompt to clipboard */
const handleCopy = () => { const handleCopy = () => {
if (!values.current_prompt || values?.current_prompt?.trim() === '') return if (!values.current_prompt || values?.current_prompt?.trim() === '') return
copy(values.current_prompt) copy(values.current_prompt)
message.success(t('common.copySuccess')) message.success(t('common.copySuccess'))
} }
/** Open variable selection modal */
const handleAdd = () => { const handleAdd = () => {
aiPromptVariableModalRef.current?.handleOpen() aiPromptVariableModalRef.current?.handleOpen()
} }
/** Insert variable into prompt editor */
const handleVariableApply = (value: string) => { const handleVariableApply = (value: string) => {
if (editorRef.current?.insertText) { if (editorRef.current?.insertText) {
editorRef.current.insertText(value) editorRef.current.insertText(value)
@@ -149,6 +175,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
form.setFieldValue('current_prompt', (values.current_prompt || '') + value) form.setFieldValue('current_prompt', (values.current_prompt || '') + value)
} }
} }
/** Apply optimized prompt and close modal */
const handleApply = () => { const handleApply = () => {
if (!values.current_prompt) { if (!values.current_prompt) {
return return
@@ -157,7 +184,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
handleClose() handleClose()
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));

View File

@@ -1,25 +1,42 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:14
*/
/**
* AI Prompt Variable Modal
* Allows users to insert variables into AI-generated prompts
* Supports autocomplete with existing variables
*/
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Select, AutoComplete, type AutoCompleteProps } from 'antd'; import { Form, AutoComplete, type AutoCompleteProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { Application } from '@/views/ApplicationManagement/types'
import type { AiPromptVariableModalRef } from '../types' import type { AiPromptVariableModalRef } from '../types'
import { createApiKey } from '@/api/apiKey';
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface AiPromptVariableModalProps { interface AiPromptVariableModalProps {
/** Callback to insert variable into prompt */
refresh: (value: string) => void; refresh: (value: string) => void;
/** List of available variables */
variables: string[]; variables: string[];
} }
/**
* Variable selection modal for AI prompt assistant
*/
const AiPromptVariableModal = forwardRef<AiPromptVariableModalRef, AiPromptVariableModalProps>(({ const AiPromptVariableModal = forwardRef<AiPromptVariableModalRef, AiPromptVariableModalProps>(({
refresh, refresh,
variables variables
}, ref) => { }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -31,6 +48,7 @@ const AiPromptVariableModal = forwardRef<AiPromptVariableModalRef, AiPromptVaria
label: `{{${key}}}` label: `{{${key}}}`
}))) })))
}, [variables]) }, [variables])
/** Search and filter variables */
const handleSearch = (value: string) => { const handleSearch = (value: string) => {
const filterKeys = variables?.filter(key => key.includes(value)) const filterKeys = variables?.filter(key => key.includes(value))
@@ -47,18 +65,19 @@ const AiPromptVariableModal = forwardRef<AiPromptVariableModalRef, AiPromptVaria
} }
} }
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
setVisible(true); setVisible(true);
form.resetFields(); form.resetFields();
}; };
// 封装保存方法,添加提交逻辑 /** Apply selected variable */
const handleSave = () => { const handleSave = () => {
const variableName = form.getFieldValue('variableName') const variableName = form.getFieldValue('variableName')
@@ -68,7 +87,7 @@ const AiPromptVariableModal = forwardRef<AiPromptVariableModalRef, AiPromptVaria
handleClose() handleClose()
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:22
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:22
*/
/**
* API Key Configuration Modal
* Allows configuring rate limits and daily usage limits for API keys
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Slider } from 'antd'; import { Form, Slider } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -7,10 +18,17 @@ import RbModal from '@/components/RbModal'
import { updateApiKey } from '@/api/apiKey'; import { updateApiKey } from '@/api/apiKey';
import type { ApiKey } from '@/views/ApiKeyManagement/types' import type { ApiKey } from '@/views/ApiKeyManagement/types'
/**
* Component props
*/
interface ApiKeyConfigModalProps { interface ApiKeyConfigModalProps {
/** Callback to refresh API key list */
refresh: () => void; refresh: () => void;
} }
const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProps>(({
/**
* Modal for configuring API key limits
*/const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProps>(({
refresh refresh
}, ref) => { }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -20,7 +38,7 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
const values = Form.useWatch<ApiKey>([], form) const values = Form.useWatch<ApiKey>([], form)
const [editVo, setEditVo] = useState<ApiKey | null>(null) const [editVo, setEditVo] = useState<ApiKey | null>(null)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
@@ -28,6 +46,7 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
setVisible(false); setVisible(false);
}; };
/** Open modal with API key data */
const handleOpen = (apiKey: ApiKey) => { const handleOpen = (apiKey: ApiKey) => {
setVisible(true); setVisible(true);
setEditVo(apiKey) setEditVo(apiKey)
@@ -36,7 +55,7 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
rate_limit: apiKey.rate_limit rate_limit: apiKey.rate_limit
}); });
}; };
// 封装保存方法,添加提交逻辑 /** Save API key configuration */
const handleSave = () => { const handleSave = () => {
if (!editVo?.id) return if (!editVo?.id) return
form.validateFields() form.validateFields()
@@ -52,7 +71,7 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
}) })
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -73,7 +92,7 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
className="rb:px-2.5!" className="rb:px-2.5!"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }} scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
> >
{/* QPS 限制(每秒请求数) */} {/* QPS limit (requests per second) */}
<> <>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2"> <div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
{t(`application.qpsLimit`)}({t('application.qpsLimitTip')}) {t(`application.qpsLimit`)}({t('application.qpsLimitTip')})
@@ -98,7 +117,7 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
</div> </div>
</div> </div>
</> </>
{/* 日调用量限制 */} {/* Daily usage limit */}
<> <>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2"> <div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
{t(`application.dailyUsageLimit`)} {t(`application.dailyUsageLimit`)}

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:25
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:25
*/
/**
* API Key Creation Modal
* Allows creating new API keys for application access
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd'; import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,11 +20,19 @@ import RbModal from '@/components/RbModal'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface ApiKeyModalProps { interface ApiKeyModalProps {
/** Callback to refresh API key list */
refresh: () => void; refresh: () => void;
/** Application data */
application?: Application | null; application?: Application | null;
} }
/**
* Modal for creating new API keys
*/
const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({ const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
refresh, refresh,
application application
@@ -24,18 +43,19 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
setVisible(true); setVisible(true);
form.resetFields(); form.resetFields();
}; };
// 封装保存方法,添加提交逻辑 /** Create new API key */
const handleSave = () => { const handleSave = () => {
if (!application) return if (!application) return
form.validateFields() form.validateFields()
@@ -58,7 +78,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
}) })
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -78,7 +98,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
layout="vertical" layout="vertical"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }} scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
> >
{/* Key 名称 */} {/* Key name */}
<FormItem <FormItem
name="name" name="name"
label={t('application.apiKeyName')} label={t('application.apiKeyName')}
@@ -89,7 +109,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
> >
<Input placeholder={t('application.apiKeyNamePlaceholder')} /> <Input placeholder={t('application.apiKeyNamePlaceholder')} />
</FormItem> </FormItem>
{/* 描述 */} {/* Description */}
<FormItem <FormItem
name="description" name="description"
label={t('application.description')} label={t('application.description')}

View File

@@ -1,13 +1,31 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:31
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:31
*/
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
/**
* Props for Card component
*/
interface CardProps { interface CardProps {
/** Card title */
title?: string | ReactNode; title?: string | ReactNode;
/** Card subtitle */
subTitle?: string | ReactNode; subTitle?: string | ReactNode;
/** Card content */
children: ReactNode; children: ReactNode;
/** Extra content in header */
extra?: ReactNode; extra?: ReactNode;
} }
/**
* Card component wrapper
* Styled card with left border accent for application configuration sections
*/
const Card: FC<CardProps> = ({ const Card: FC<CardProps> = ({
title, title,
subTitle, subTitle,

View File

@@ -1,7 +1,20 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:39
*/
/**
* Chat debugging component for application testing
* Supports both single agent and multi-agent cluster modes
* Provides real-time streaming responses and conversation history
*/
import { type FC, useEffect, useState } from 'react'; import { type FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx' import clsx from 'clsx'
import { Input, Form } from 'antd' import { Input, Form } from 'antd'
import ChatIcon from '@/assets/images/application/chat.png' import ChatIcon from '@/assets/images/application/chat.png'
import ChatSendIcon from '@/assets/images/application/chatSend.svg' import ChatSendIcon from '@/assets/images/application/chatSend.svg'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png' import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
@@ -12,13 +25,26 @@ import ChatContent from '@/components/Chat/ChatContent'
import type { ChatItem } from '@/components/Chat/types' import type { ChatItem } from '@/components/Chat/types'
import { type SSEMessage } from '@/utils/stream' import { type SSEMessage } from '@/utils/stream'
/**
* Component props
*/
interface ChatProps { interface ChatProps {
/** List of chat configurations for comparison */
chatList: ChatData[]; chatList: ChatData[];
/** Application configuration data */
data: Config; data: Config;
/** Update chat list state */
updateChatList: React.Dispatch<React.SetStateAction<ChatData[]>>; updateChatList: React.Dispatch<React.SetStateAction<ChatData[]>>;
/** Save configuration before running */
handleSave: (flag?: boolean) => Promise<unknown>; handleSave: (flag?: boolean) => Promise<unknown>;
/** Source type: multi-agent cluster or single agent */
source?: 'multi_agent' | 'agent'; source?: 'multi_agent' | 'agent';
} }
/**
* Chat debugging component
* Allows testing application with different model configurations side-by-side
*/
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => { const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm<{ message: string }>() const [form] = Form.useForm<{ message: string }>()
@@ -31,6 +57,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
setIsCluster(source === 'multi_agent') setIsCluster(source === 'multi_agent')
}, [source]) }, [source])
/** Add user message to all chat lists */
const addUserMessage = (message: string) => { const addUserMessage = (message: string) => {
const newUserMessage: ChatItem = { const newUserMessage: ChatItem = {
role: 'user', role: 'user',
@@ -42,6 +69,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
list: [...(item.list || []), newUserMessage] list: [...(item.list || []), newUserMessage]
}))) })))
} }
/** Add empty assistant message placeholder */
const addAssistantMessage = () => { const addAssistantMessage = () => {
const assistantMessage: ChatItem = { const assistantMessage: ChatItem = {
role: 'assistant', role: 'assistant',
@@ -65,6 +93,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}))) })))
} }
} }
/** Update assistant message with streaming content */
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => { const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
if (!content || !model_config_id) return if (!content || !model_config_id) return
updateChatList(prev => { updateChatList(prev => {
@@ -92,6 +121,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
return prev; return prev;
}) })
} }
/** Update assistant message when error occurs */
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => { const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
if (message_length > 0 || !model_config_id) return if (message_length > 0 || !model_config_id) return
@@ -120,6 +150,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
return prev return prev
}) })
} }
/** Send message for agent comparison mode */
const handleSend = () => { const handleSend = () => {
if (loading) return if (loading) return
setLoading(true) setLoading(true)
@@ -176,6 +207,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}) })
} }
/** Add assistant message for cluster mode */
const addClusterAssistantMessage = () => { const addClusterAssistantMessage = () => {
const assistantMessage: ChatItem = { const assistantMessage: ChatItem = {
role: 'assistant', role: 'assistant',
@@ -187,6 +219,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
list: [...(item.list || []), assistantMessage] list: [...(item.list || []), assistantMessage]
}))) })))
} }
/** Update cluster assistant message with content */
const updateClusterAssistantMessage = (content?: string) => { const updateClusterAssistantMessage = (content?: string) => {
if (!content) return if (!content) return
updateChatList(prev => { updateChatList(prev => {
@@ -209,6 +242,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
return [...modelChatList] return [...modelChatList]
}) })
} }
/** Update cluster message when error occurs */
const updateClusterErrorAssistantMessage = (message_length: number) => { const updateClusterErrorAssistantMessage = (message_length: number) => {
if (message_length > 0) return if (message_length > 0) return
@@ -232,6 +266,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
return [...modelChatList] return [...modelChatList]
}) })
} }
/** Send message for cluster mode */
const handleClusterSend = () => { const handleClusterSend = () => {
if (loading) return if (loading) return
setLoading(true) setLoading(true)
@@ -291,6 +326,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}) })
} }
/** Delete chat configuration from list */
const handleDelete = (index: number) => { const handleDelete = (index: number) => {
updateChatList(chatList.filter((_, voIndex) => voIndex !== index)) updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
} }

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:44
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:44
*/
/**
* Chat Variable Configuration Modal
* Allows users to configure variable values before starting a chat session
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, InputNumber } from 'antd'; import { Form, Input, InputNumber } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -6,10 +17,17 @@ import type { ChatVariableConfigModalRef } from '../types'
import type { Variable } from './VariableList/types' import type { Variable } from './VariableList/types'
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
/**
* Component props
*/
interface VariableEditModalProps { interface VariableEditModalProps {
/** Callback to update variables */
refresh: (values: Variable[]) => void; refresh: (values: Variable[]) => void;
} }
/**
* Modal for configuring chat variables
*/
const ChatVariableConfigModal = forwardRef<ChatVariableConfigModalRef, VariableEditModalProps>(({ const ChatVariableConfigModal = forwardRef<ChatVariableConfigModalRef, VariableEditModalProps>(({
refresh, refresh,
}, ref) => { }, ref) => {
@@ -19,20 +37,21 @@ const ChatVariableConfigModal = forwardRef<ChatVariableConfigModalRef, VariableE
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [initialValues, setInitialValues] = useState<Variable[]>([]) const [initialValues, setInitialValues] = useState<Variable[]>([])
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal with variable list */
const handleOpen = (values: Variable[]) => { const handleOpen = (values: Variable[]) => {
console.log('values', values) console.log('values', values)
setVisible(true); setVisible(true);
form.setFieldsValue({variables: values}) form.setFieldsValue({variables: values})
setInitialValues([...values]) setInitialValues([...values])
}; };
// 封装保存方法,添加提交逻辑 /** Save variable configuration */
const handleSave = () => { const handleSave = () => {
form.validateFields().then((values) => { form.validateFields().then((values) => {
refresh([ refresh([
@@ -42,7 +61,7 @@ const ChatVariableConfigModal = forwardRef<ChatVariableConfigModalRef, VariableE
}) })
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,8 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:52
*/
import { type FC, useRef } from 'react'; import { type FC, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd'; import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styles from '../index.module.css' import styles from '../index.module.css'
import logoutIcon from '@/assets/images/logout.svg' import logoutIcon from '@/assets/images/logout.svg'
import editIcon from '@/assets/images/edit_hover.svg' import editIcon from '@/assets/images/edit_hover.svg'
@@ -17,21 +24,43 @@ import CopyModal from './CopyModal'
const { Header } = Layout; const { Header } = Layout;
/**
* Tab keys for application configuration
*/
const tabKeys = ['arrangement', 'api', 'release', 'statistics'] const tabKeys = ['arrangement', 'api', 'release', 'statistics']
/**
* Menu icon mapping
*/
const menuIcons: Record<string, string> = { const menuIcons: Record<string, string> = {
edit: editIcon, edit: editIcon,
copy: copyIcon, copy: copyIcon,
export: exportIcon, export: exportIcon,
delete: deleteIcon delete: deleteIcon
} }
/**
* Props for ConfigHeader component
*/
interface ConfigHeaderProps { interface ConfigHeaderProps {
/** Application data */
application?: Application; application?: Application;
/** Active tab key */
activeTab: string; activeTab: string;
/** Tab change handler */
handleChangeTab: (key: string) => void; handleChangeTab: (key: string) => void;
/** Refresh application data */
refresh: () => void; refresh: () => void;
/** Workflow component ref */
workflowRef: React.RefObject<WorkflowRef> workflowRef: React.RefObject<WorkflowRef>
/** App component ref (Agent/Cluster/Workflow) */
appRef?: React.RefObject<AgentRef | ClusterRef | WorkflowRef> appRef?: React.RefObject<AgentRef | ClusterRef | WorkflowRef>
} }
/**
* Configuration header component
* Displays application name, tabs, and action buttons
*/
const ConfigHeader: FC<ConfigHeaderProps> = ({ const ConfigHeader: FC<ConfigHeaderProps> = ({
application, activeTab, handleChangeTab, refresh, application, activeTab, handleChangeTab, refresh,
workflowRef, workflowRef,
@@ -42,12 +71,18 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
const applicationModalRef = useRef<ApplicationModalRef>(null); const applicationModalRef = useRef<ApplicationModalRef>(null);
const copyModalRef = useRef<CopyModalRef>(null); const copyModalRef = useRef<CopyModalRef>(null);
/**
* Format tab items for display
*/
const formatTabItems = () => { const formatTabItems = () => {
return tabKeys.map(key => ({ return tabKeys.map(key => ({
key, key,
label: t(`application.${key}`), label: t(`application.${key}`),
})) }))
} }
/**
* Format dropdown menu items
*/
const formatMenuItems = () => { const formatMenuItems = () => {
const items = ['edit', 'copy', 'export', 'delete'].map(key => ({ const items = ['edit', 'copy', 'export', 'delete'].map(key => ({
key, key,
@@ -59,6 +94,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
onClick: handleClick onClick: handleClick
} }
} }
/**
* Handle menu item click
*/
const handleClick: MenuProps['onClick'] = ({ key }) => { const handleClick: MenuProps['onClick'] = ({ key }) => {
switch (key) { switch (key) {
case 'edit': case 'edit':
@@ -74,6 +112,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
break; break;
} }
} }
/**
* Delete application with confirmation
*/
const handleDelete = () => { const handleDelete = () => {
if (!id) { if (!id) {
return return
@@ -86,21 +127,36 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
console.error('Failed to delete application'); console.error('Failed to delete application');
}); });
} }
/**
* Navigate to application list
*/
const goToApplication = () => { const goToApplication = () => {
navigate('/application', { replace: true }) navigate('/application', { replace: true })
} }
/**
* Save workflow configuration
*/
const save = () => { const save = () => {
workflowRef.current?.handleSave() workflowRef.current?.handleSave()
} }
/**
* Run workflow
*/
const run = () => { const run = () => {
workflowRef.current?.handleSave(false) workflowRef.current?.handleSave(false)
.then(() => { .then(() => {
workflowRef.current?.handleRun() workflowRef.current?.handleRun()
}) })
} }
/**
* Clear workflow canvas
*/
const clear = () => { const clear = () => {
workflowRef?.current?.graphRef?.current?.clearCells() workflowRef?.current?.graphRef?.current?.clearCells()
} }
/**
* Add variable to workflow
*/
const addvariable = () => { const addvariable = () => {
workflowRef?.current?.addVariable() workflowRef?.current?.addVariable()
} }

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:56
*/
/**
* Copy Application Modal
* Allows users to duplicate an existing application with a new name
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -10,10 +21,17 @@ import type { Application } from '@/views/ApplicationManagement/types'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface CopyModalProps { interface CopyModalProps {
/** Application data to copy */
data: Application data: Application
} }
/**
* Modal for copying applications
*/
const CopyModal = forwardRef<CopyModalRef, CopyModalProps>(({ const CopyModal = forwardRef<CopyModalRef, CopyModalProps>(({
data data
}, ref) => { }, ref) => {
@@ -23,17 +41,18 @@ const CopyModal = forwardRef<CopyModalRef, CopyModalProps>(({
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Copy application with new name */
const handleSave = () => { const handleSave = () => {
setVisible(false); setVisible(false);
setLoading(true) setLoading(true)
@@ -48,7 +67,7 @@ const CopyModal = forwardRef<CopyModalRef, CopyModalProps>(({
}) })
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -68,7 +87,7 @@ const CopyModal = forwardRef<CopyModalRef, CopyModalProps>(({
form={form} form={form}
layout="vertical" layout="vertical"
> >
{/* 应用名 */} {/* Application name */}
<FormItem <FormItem
name="new_name" name="new_name"
label={t('application.applicationName')} label={t('application.applicationName')}

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:17
*/
/**
* Rich text editor component using Lexical framework
* Provides text editing with insert, append, clear, and scroll capabilities
*/
import {forwardRef, useImperativeHandle } from 'react'; import {forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { LexicalComposer } from '@lexical/react/LexicalComposer';
@@ -6,25 +17,44 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { $getSelection, $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode, $isTextNode } from 'lexical'; import { $getSelection, $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode, $isTextNode } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import InitialValuePlugin from './plugin/InitialValuePlugin' import InitialValuePlugin from './plugin/InitialValuePlugin'
import LineBreakPlugin from './plugin/LineBreakPlugin'; import LineBreakPlugin from './plugin/LineBreakPlugin';
import InsertTextPlugin from './plugin/InsertTextPlugin'; import InsertTextPlugin from './plugin/InsertTextPlugin';
/**
* Editor ref methods exposed to parent components
*/
export interface EditorRef { export interface EditorRef {
/** Insert text at current cursor position */
insertText: (text: string) => void; insertText: (text: string) => void;
/** Append text to the end of content */
appendText: (text: string) => void; appendText: (text: string) => void;
/** Clear all editor content */
clear: () => void; clear: () => void;
/** Scroll editor to bottom */
scrollToBottom: () => void; scrollToBottom: () => void;
} }
/**
* Editor component props
*/
interface LexicalEditorProps { interface LexicalEditorProps {
/** Additional CSS class names */
className?: string; className?: string;
/** Placeholder text when editor is empty */
placeholder?: string; placeholder?: string;
/** Initial editor value */
value?: string; value?: string;
/** Callback when content changes */
onChange?: (value: string) => void; onChange?: (value: string) => void;
/** Editor height in pixels */
height?: number; height?: number;
} }
/**
* Lexical editor theme configuration
*/
const theme = { const theme = {
paragraph: 'editor-paragraph', paragraph: 'editor-paragraph',
text: { text: {
@@ -33,14 +63,24 @@ const theme = {
}, },
}; };
/**
* Editor content component with Lexical context
*/
const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
className = '', className = '',
value, value,
placeholder = "请输入内容...", placeholder = "Please enter content...",
onChange, onChange,
}, ref) => { }, ref) => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
/**
* Expose editor methods to parent component
* - insertText: Insert at cursor position
* - appendText: Append to end of content
* - clear: Clear all content
* - scrollToBottom: Scroll to bottom
*/
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
insertText: (text: string) => { insertText: (text: string) => {
editor.update(() => { editor.update(() => {
@@ -109,6 +149,10 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
); );
}); });
/**
* Main editor wrapper component
* Initializes Lexical composer with configuration
*/
const Editor = forwardRef<EditorRef, LexicalEditorProps>((props, ref) => { const Editor = forwardRef<EditorRef, LexicalEditorProps>((props, ref) => {
const initialConfig = { const initialConfig = {
namespace: 'Editor', namespace: 'Editor',

View File

@@ -1,20 +1,34 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:24:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:24:59
*/
/**
* Initial Value Plugin
* Sets the initial content of the Lexical editor
* Only updates when the value prop changes
*/
import { type FC, useEffect, useRef } from 'react'; import { type FC, useEffect, useRef } from 'react';
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
// 设置初始值的插件 /**
* Plugin to set initial editor value
*/
const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => { const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
const lastValueRef = useRef<string | undefined>(undefined); const lastValueRef = useRef<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
// 只有当value真正发生变化时才更新 // Only update when value actually changes
if (lastValueRef.current !== value) { if (lastValueRef.current !== value) {
editor.update(() => { editor.update(() => {
const root = $getRoot(); const root = $getRoot();
const currentText = root.getTextContent(); const currentText = root.getTextContent();
// 如果当前内容和新值相同,则不更新 // If current content matches new value, don't update
if (currentText === (value || '')) { if (currentText === (value || '')) {
return; return;
} }
@@ -26,7 +40,7 @@ const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => {
paragraph.append(textNode); paragraph.append(textNode);
root.append(paragraph); root.append(paragraph);
} else { } else {
// valueundefined或空时,创建一个空段落 // When value is undefined or empty, create an empty paragraph
const paragraph = $createParagraphNode(); const paragraph = $createParagraphNode();
root.append(paragraph); root.append(paragraph);
} }

View File

@@ -1,10 +1,22 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:05
*/
/**
* Insert Text Plugin
* Provides functionality to insert text at the current cursor position
*/
import { forwardRef, useImperativeHandle } from 'react'; import { forwardRef, useImperativeHandle } from 'react';
import { $getSelection } from 'lexical'; import { $getSelection } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import type { EditorRef } from '../index'
// 插入文本的插件 /**
const InsertTextPlugin = forwardRef<EditorRef>((_, ref) => { * Plugin to insert text at cursor position
*/
const InsertTextPlugin = forwardRef<{ insertText: (text: string) => void; }>((_, ref) => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({

View File

@@ -1,8 +1,22 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:09
*/
/**
* Line Break Plugin
* Handles line breaks and triggers onChange callback when editor content changes
* Converts \n escape sequences to actual line breaks
*/
import { type FC, useEffect } from 'react'; import { type FC, useEffect } from 'react';
import { $getRoot } from 'lexical'; import { $getRoot } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
// 处理换行的插件 /**
* Plugin to handle line breaks and content changes
*/
const LineBreakPlugin: FC<{ onChange?: (value: string) => void }> = ({ onChange }) => { const LineBreakPlugin: FC<{ onChange?: (value: string) => void }> = ({ onChange }) => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
@@ -11,7 +25,7 @@ const LineBreakPlugin: FC<{ onChange?: (value: string) => void }> = ({ onChange
editorState.read(() => { editorState.read(() => {
const root = $getRoot(); const root = $getRoot();
const textContent = root.getTextContent(); const textContent = root.getTextContent();
// 将\n转换为实际换行 // Convert \n to actual line breaks
const processedContent = textContent.replace(/\\n/g, '\n'); const processedContent = textContent.replace(/\\n/g, '\n');
onChange?.(processedContent); onChange?.(processedContent);
}); });

View File

@@ -1,6 +1,19 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:32
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:32
*/
/**
* Knowledge Base Component
* Manages knowledge base associations for the application
* Allows adding, configuring, and removing knowledge bases
*/
import { type FC, useRef, useState, useEffect } from 'react' import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Space, Button, List } from 'antd' import { Space, Button, List } from 'antd'
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg' import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
import type { import type {
KnowledgeConfigForm, KnowledgeConfigForm,
@@ -19,6 +32,11 @@ import Tag from '@/components/Tag'
import { getKnowledgeBaseList } from '@/api/knowledgeBase' import { getKnowledgeBaseList } from '@/api/knowledgeBase'
import Card from '../Card' import Card from '../Card'
/**
* Knowledge base management component
* @param value - Current knowledge configuration
* @param onChange - Callback when configuration changes
*/
const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfig) => void}> = ({value = {knowledge_bases: []}, onChange}) => { const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfig) => void}> = ({value = {knowledge_bases: []}, onChange}) => {
const { t } = useTranslation() const { t } = useTranslation()
const knowledgeModalRef = useRef<KnowledgeModalRef>(null) const knowledgeModalRef = useRef<KnowledgeModalRef>(null)
@@ -32,10 +50,10 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
setEditConfig({ ...(value || {}) }) setEditConfig({ ...(value || {}) })
const knowledge_bases = [...(value.knowledge_bases || [])] const knowledge_bases = [...(value.knowledge_bases || [])]
// 检查是否有knowledge_bases缺少name字段 // Check if knowledge_bases are missing name field
const basesWithoutName = knowledge_bases.filter(base => !base.name) const basesWithoutName = knowledge_bases.filter(base => !base.name)
if (basesWithoutName.length > 0) { if (basesWithoutName.length > 0) {
// 调用接口获取完整的知识库信息 // Call API to get complete knowledge base information
getKnowledgeBaseList().then(res => { getKnowledgeBaseList().then(res => {
const fullBases = knowledge_bases.map(base => { const fullBases = knowledge_bases.map(base => {
if (!base.name) { if (!base.name) {
@@ -54,12 +72,15 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
} }
}, [value]) }, [value])
/** Open global knowledge configuration modal */
const handleKnowledgeConfig = () => { const handleKnowledgeConfig = () => {
knowledgeGlobalConfigModalRef.current?.handleOpen() knowledgeGlobalConfigModalRef.current?.handleOpen()
} }
/** Open knowledge base selection modal */
const handleAddKnowledge = () => { const handleAddKnowledge = () => {
knowledgeModalRef.current?.handleOpen() knowledgeModalRef.current?.handleOpen()
} }
/** Remove knowledge base from list */
const handleDeleteKnowledge = (id: string) => { const handleDeleteKnowledge = (id: string) => {
const list = knowledgeList.filter(item => item.id !== id) const list = knowledgeList.filter(item => item.id !== id)
setKnowledgeList([...list]) setKnowledgeList([...list])
@@ -68,9 +89,11 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
knowledge_bases: [...list], knowledge_bases: [...list],
}) })
} }
/** Open knowledge base configuration modal */
const handleEditKnowledge = (item: KnowledgeBase) => { const handleEditKnowledge = (item: KnowledgeBase) => {
knowledgeConfigModalRef.current?.handleOpen(item) knowledgeConfigModalRef.current?.handleOpen(item)
} }
/** Update knowledge configuration */
const refresh = (values: KnowledgeBase[] | KnowledgeConfigForm | RerankerConfig, type: 'knowledge' | 'knowledgeConfig' | 'rerankerConfig') => { const refresh = (values: KnowledgeBase[] | KnowledgeConfigForm | RerankerConfig, type: 'knowledge' | 'knowledgeConfig' | 'rerankerConfig') => {
if (type === 'knowledge') { if (type === 'knowledge') {
let list = [...knowledgeList] let list = [...knowledgeList]

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:37
*/
/**
* Knowledge Configuration Modal
* Configures retrieval settings for individual knowledge bases
* Supports different retrieval modes: participle, semantic, and hybrid
*/
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Form, Select, InputNumber } from 'antd'; import { Form, Select, InputNumber } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,11 +21,22 @@ import { formatDateTime } from '@/utils/format';
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface KnowledgeConfigModalProps { interface KnowledgeConfigModalProps {
/** Callback to update knowledge configuration */
refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void; refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void;
} }
/**
* Available retrieval types
*/
const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid'] const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid']
/**
* Modal for configuring knowledge base retrieval settings
*/
const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfigModalProps>(({ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfigModalProps>(({
refresh, refresh,
}, ref) => { }, ref) => {
@@ -24,13 +47,14 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
const values = Form.useWatch<KnowledgeConfigForm>([], form); const values = Form.useWatch<KnowledgeConfigForm>([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setData(null) setData(null)
}; };
/** Open modal with knowledge base data */
const handleOpen = (data: KnowledgeBase) => { const handleOpen = (data: KnowledgeBase) => {
form.setFieldsValue({ form.setFieldsValue({
retrieve_type: data?.config?.retrieve_type || retrieveTypes[0], retrieve_type: data?.config?.retrieve_type || retrieveTypes[0],
@@ -44,7 +68,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
setData({...data}) setData({...data})
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save knowledge configuration */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -57,7 +81,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
}); });
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -94,7 +118,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
</div> </div>
)} )}
<FormItem name="kb_id" hidden /> <FormItem name="kb_id" hidden />
{/* 检索模式 */} {/* Retrieval mode */}
<FormItem <FormItem
name="retrieve_type" name="retrieve_type"
label={t('application.retrieve_type')} label={t('application.retrieve_type')}
@@ -124,7 +148,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
onChange={(value) => form.setFieldValue('top_k', value)} onChange={(value) => form.setFieldValue('top_k', value)}
/> />
</FormItem> </FormItem>
{/* 语义相似度阈值 similarity_threshold */} {/* Semantic similarity threshold */}
{values?.retrieve_type === 'semantic' && ( {values?.retrieve_type === 'semantic' && (
<FormItem <FormItem
name="similarity_threshold" name="similarity_threshold"
@@ -139,7 +163,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
/> />
</FormItem> </FormItem>
)} )}
{/* 分词匹配度阈值 vector_similarity_weight */} {/* Word segmentation matching threshold */}
{values?.retrieve_type === 'participle' && ( {values?.retrieve_type === 'participle' && (
<FormItem <FormItem
name="vector_similarity_weight" name="vector_similarity_weight"
@@ -154,7 +178,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
/> />
</FormItem> </FormItem>
)} )}
{/* 混合检索权重 */} {/* Hybrid retrieval weight */}
{values?.retrieve_type === 'hybrid' && ( {values?.retrieve_type === 'hybrid' && (
<> <>
<FormItem <FormItem

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:42
*/
/**
* Knowledge Global Configuration Modal
* Configures global reranker settings for all knowledge bases
*/
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { Form, InputNumber, Switch } from 'antd'; import { Form, InputNumber, Switch } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,11 +20,19 @@ import { getModelListUrl } from '@/api/models'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface KnowledgeGlobalConfigModalProps { interface KnowledgeGlobalConfigModalProps {
/** Current reranker configuration */
data: RerankerConfig; data: RerankerConfig;
/** Callback to update reranker configuration */
refresh: (values: RerankerConfig, type: 'rerankerConfig') => void; refresh: (values: RerankerConfig, type: 'rerankerConfig') => void;
} }
/**
* Modal for configuring global reranker settings
*/
const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, KnowledgeGlobalConfigModalProps>(({ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, KnowledgeGlobalConfigModalProps>(({
refresh, refresh,
data, data,
@@ -23,17 +42,18 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
const [form] = Form.useForm<RerankerConfig>(); const [form] = Form.useForm<RerankerConfig>();
const values = Form.useWatch<RerankerConfig>([], form); const values = Form.useWatch<RerankerConfig>([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
}; };
/** Open modal with current configuration */
const handleOpen = () => { const handleOpen = () => {
form.setFieldsValue({ ...data, rerank_model: !!data?.reranker_id }) form.setFieldsValue({ ...data, rerank_model: !!data?.reranker_id })
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save reranker configuration */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -54,7 +74,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
} }
}, [values?.rerank_model]) }, [values?.rerank_model])
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));
@@ -73,7 +93,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
> >
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div> <div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div>
{/* 结果重排 */} {/* Result reranking */}
<div className="rb:flex rb:items-center rb:justify-between rb:my-6"> <div className="rb:flex rb:items-center rb:justify-between rb:my-6">
<div className="rb:text-[14px] rb:font-medium rb:leading-5"> <div className="rb:text-[14px] rb:font-medium rb:leading-5">
{t('application.rerankModel')} {t('application.rerankModel')}

View File

@@ -1,7 +1,19 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:49
*/
/**
* Knowledge List Modal
* Displays and allows selection of knowledge bases to associate with the application
*/
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Space, List } from 'antd'; import { Space, List } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx' import clsx from 'clsx'
import type { KnowledgeModalRef, KnowledgeBase } from './types' import type { KnowledgeModalRef, KnowledgeBase } from './types'
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types' import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
@@ -9,11 +21,19 @@ import { getKnowledgeBaseList } from '@/api/knowledgeBase'
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
/**
* Component props
*/
interface KnowledgeModalProps { interface KnowledgeModalProps {
/** Callback to add selected knowledge bases */
refresh: (rows: KnowledgeBase[], type: 'knowledge') => void; refresh: (rows: KnowledgeBase[], type: 'knowledge') => void;
/** Currently selected knowledge bases */
selectedList: KnowledgeBase[]; selectedList: KnowledgeBase[];
} }
/**
* Modal for selecting knowledge bases
*/
const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
refresh, refresh,
selectedList selectedList
@@ -26,7 +46,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
const [selectedIds, setSelectedIds] = useState<string[]>([]) const [selectedIds, setSelectedIds] = useState<string[]>([])
const [selectedRows, setSelectedRows] = useState<KnowledgeBase[]>([]) const [selectedRows, setSelectedRows] = useState<KnowledgeBase[]>([])
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
setQuery({}) setQuery({})
@@ -34,6 +54,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
setSelectedRows([]) setSelectedRows([])
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
setVisible(true); setVisible(true);
setQuery({}) setQuery({})
@@ -46,6 +67,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
getList() getList()
} }
}, [query.keywords, visible]) }, [query.keywords, visible])
/** Fetch knowledge base list */
const getList = () => { const getList = () => {
getKnowledgeBaseList(undefined, { getKnowledgeBaseList(undefined, {
...query, ...query,
@@ -58,7 +80,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
setList(response.items || []) setList(response.items || [])
}) })
} }
// 封装保存方法,添加提交逻辑 /** Save selected knowledge bases */
const handleSave = () => { const handleSave = () => {
refresh(selectedRows.map(item => ({ refresh(selectedRows.map(item => ({
...item, ...item,
@@ -72,16 +94,18 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
setVisible(false); setVisible(false);
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
})); }));
/** Search knowledge bases */
const handleSearch = (value?: string) => { const handleSearch = (value?: string) => {
setQuery({keywords: value}) setQuery({keywords: value})
setSelectedIds([]) setSelectedIds([])
setSelectedRows([]) setSelectedRows([])
} }
/** Toggle knowledge base selection */
const handleSelect = (item: KnowledgeBase) => { const handleSelect = (item: KnowledgeBase) => {
const index = selectedIds.indexOf(item.id) const index = selectedIds.indexOf(item.id)
if (index === -1) { if (index === -1) {

View File

@@ -1,30 +1,87 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:53
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:53
*/
/**
* Type definitions for knowledge base configuration in application settings
*/
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types' import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
/**
* Reranker configuration for knowledge retrieval
*/
export interface RerankerConfig { export interface RerankerConfig {
/** Whether to enable rerank model */
rerank_model?: boolean | undefined; rerank_model?: boolean | undefined;
/** Reranker model ID */
reranker_id?: string | undefined; reranker_id?: string | undefined;
/** Top K results for reranking */
reranker_top_k?: number | undefined; reranker_top_k?: number | undefined;
} }
/**
* Knowledge retrieval type
* - participle: Word segmentation based retrieval
* - semantic: Semantic similarity based retrieval
* - hybrid: Combination of both methods
*/
export type RetrieveType = 'participle' | 'semantic' | 'hybrid' export type RetrieveType = 'participle' | 'semantic' | 'hybrid'
/**
* Knowledge base configuration form data
*/
export interface KnowledgeConfigForm { export interface KnowledgeConfigForm {
/** Knowledge base ID */
kb_id?: string; kb_id?: string;
/** Similarity threshold for retrieval (0-1) */
similarity_threshold?: number; similarity_threshold?: number;
/** Weight for vector similarity in hybrid mode (0-1) */
vector_similarity_weight?: number; vector_similarity_weight?: number;
/** Number of top results to retrieve */
top_k?: number; top_k?: number;
/** Retrieval strategy type */
retrieve_type?: RetrieveType; retrieve_type?: RetrieveType;
} }
/**
* Knowledge base with configuration
*/
export interface KnowledgeBase extends KnowledgeBaseListItem, KnowledgeConfigForm { export interface KnowledgeBase extends KnowledgeBaseListItem, KnowledgeConfigForm {
/** Additional configuration object */
config?: KnowledgeConfigForm config?: KnowledgeConfigForm
} }
/**
* Complete knowledge configuration including reranker settings
*/
export interface KnowledgeConfig extends RerankerConfig { export interface KnowledgeConfig extends RerankerConfig {
/** List of configured knowledge bases */
knowledge_bases: KnowledgeBase[]; knowledge_bases: KnowledgeBase[];
} }
/**
* Modal ref for individual knowledge base configuration
*/
export interface KnowledgeConfigModalRef { export interface KnowledgeConfigModalRef {
/** Open modal with knowledge base data */
handleOpen: (data: KnowledgeBase) => void; handleOpen: (data: KnowledgeBase) => void;
} }
/**
* Modal ref for global knowledge configuration
*/
export interface KnowledgeGlobalConfigModalRef { export interface KnowledgeGlobalConfigModalRef {
/** Open global configuration modal */
handleOpen: () => void; handleOpen: () => void;
} }
/**
* Modal ref for knowledge base selection
*/
export interface KnowledgeModalRef { export interface KnowledgeModalRef {
/** Open modal with optional existing configuration */
handleOpen: (config?: KnowledgeConfig[]) => void; handleOpen: (config?: KnowledgeConfig[]) => void;
} }

View File

@@ -1,18 +1,38 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:03
*/
/**
* Line chart card component for displaying statistics
* Uses ECharts to render time-series data with gradient area fill
*/
import { type FC, useEffect, useRef } from 'react' import { type FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'; import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import Empty from '@/components/Empty'
import Empty from '@/components/Empty'
import Card from './Card' import Card from './Card'
import type { StatisticsItem } from '../types' import type { StatisticsItem } from '../types'
/**
* Component props
*/
interface LineCardProps { interface LineCardProps {
/** Chart data points */
chartData: StatisticsItem[]; chartData: StatisticsItem[];
/** Statistics type key */
type: string; type: string;
/** Total count to display */
total: number; total: number;
} }
/**
* ECharts series configuration
*/
const SeriesConfig = { const SeriesConfig = {
type: 'line', type: 'line',
stack: 'Total', stack: 'Total',
@@ -30,6 +50,9 @@ const SeriesConfig = {
}, },
} }
/**
* Color mapping for different statistic types
*/
const ColorObj: Record<string, string> = { const ColorObj: Record<string, string> = {
daily_conversations: '#FFB048', daily_conversations: '#FFB048',
daily_new_users: '#4DA8FF', daily_new_users: '#4DA8FF',
@@ -37,6 +60,10 @@ const ColorObj: Record<string, string> = {
daily_tokens: '#AD88FF' daily_tokens: '#AD88FF'
} }
/**
* Line chart card component
* Displays time-series statistics with gradient area chart
*/
const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => { const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => {
const { t } = useTranslation() const { t } = useTranslation()
const chartRef = useRef<ReactEcharts>(null); const chartRef = useRef<ReactEcharts>(null);

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:07
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:07
*/
/**
* Model Configuration Modal
* Allows configuring model parameters like temperature, max_tokens, top_p, etc.
* Supports different sources: model, chat, and multi_agent
*/
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { Form, Select } from 'antd'; import { Form, Select } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,12 +21,24 @@ import RbSlider from '@/components/RbSlider'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface ModelConfigModalProps { interface ModelConfigModalProps {
/** List of available models */
modelList?: ModelListItem[]; modelList?: ModelListItem[];
/** Callback to update model configuration */
refresh: (values: ModelConfig, type: Source) => void; refresh: (values: ModelConfig, type: Source) => void;
/** Application configuration data */
data: Config; data: Config;
} }
/**
* Modal for configuring model parameters
*/
/**
* Model parameter configuration fields
*/
const configFields = [ const configFields = [
{ key: 'temperature', max: 2, min: 0, step: 0.1, defaultValue: 0.7 }, { key: 'temperature', max: 2, min: 0, step: 0.1, defaultValue: 0.7 },
{ key: 'max_tokens', max: 32000, min: 256, step: 1, defaultValue: 2000 }, { key: 'max_tokens', max: 32000, min: 256, step: 1, defaultValue: 2000 },
@@ -36,12 +60,13 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
}; };
/** Open modal with configuration source */
const handleOpen = (source: Source, model?: any) => { const handleOpen = (source: Source, model?: any) => {
setSource(source) setSource(source)
if (source === 'model') { if (source === 'model') {
@@ -64,7 +89,7 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
} }
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save model configuration */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -76,13 +101,14 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
console.log('err', err) console.log('err', err)
}); });
} }
/** Handle model selection change */
const handleChange = (_value: string, option: ModelListItem | ModelListItem[] | undefined) => { const handleChange = (_value: string, option: ModelListItem | ModelListItem[] | undefined) => {
if (source === 'chat') { if (source === 'chat') {
form.setFieldValue('label', (option as ModelListItem).name) form.setFieldValue('label', (option as ModelListItem).name)
} }
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:11
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:11
*/
/**
* Release Modal
* Allows publishing a new version of the application
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,11 +20,19 @@ import type { Application } from '@/views/ApplicationManagement/types'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface ReleaseModalProps { interface ReleaseModalProps {
/** Callback to refresh release list */
refreshTable: () => void; refreshTable: () => void;
/** Application data */
data: Application data: Application
} }
/**
* Modal for publishing new application versions
*/
const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({ const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({
refreshTable, refreshTable,
data data
@@ -23,17 +42,18 @@ const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Publish new release */
const handleSave = () => { const handleSave = () => {
form.validateFields().then(() => { form.validateFields().then(() => {
setLoading(true) setLoading(true)
@@ -49,7 +69,7 @@ const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({
}) })
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -69,7 +89,7 @@ const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({
form={form} form={form}
layout="vertical" layout="vertical"
> >
{/* 版本名 */} {/* Version name */}
<FormItem <FormItem
name="version_name" name="version_name"
label={t('application.versionName')} label={t('application.versionName')}
@@ -80,7 +100,7 @@ const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({
> >
<Input placeholder={t('common.enter')} /> <Input placeholder={t('common.enter')} />
</FormItem> </FormItem>
{/* 版本描述 */} {/* Version description */}
<FormItem <FormItem
name="release_notes" name="release_notes"
label={t('application.versionDescription')} label={t('application.versionDescription')}

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:46
*/
/**
* Release Share Modal
* Generates and displays a shareable link for a specific application version
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Button, App } from 'antd'; import { Button, App } from 'antd';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { ExclamationCircleFilled } from '@ant-design/icons';
@@ -9,10 +20,17 @@ import RbModal from '@/components/RbModal'
import { shareRelease } from '@/api/application' import { shareRelease } from '@/api/application'
import RbAlert from '@/components/RbAlert' import RbAlert from '@/components/RbAlert'
/**
* Component props
*/
interface ReleaseShareModalProps { interface ReleaseShareModalProps {
/** Version to share */
version: Release | null version: Release | null
} }
/**
* Modal for sharing application versions
*/
const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProps>(({ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProps>(({
version version
}, ref) => { }, ref) => {
@@ -22,12 +40,13 @@ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProp
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [shareLink, setShareLink] = useState<string | null>(null) const [shareLink, setShareLink] = useState<string | null>(null)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
setLoading(false) setLoading(false)
}; };
/** Open modal and generate share link */
const handleOpen = () => { const handleOpen = () => {
if (!version) { if (!version) {
return return
@@ -45,13 +64,14 @@ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProp
setLoading(false) setLoading(false)
}) })
}; };
/** Copy share link to clipboard */
const handleCopy = () => { const handleCopy = () => {
if (!shareLink) return if (!shareLink) return
copy(shareLink) copy(shareLink)
message.success(t('common.copySuccess')) message.success(t('common.copySuccess'))
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -65,9 +85,9 @@ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProp
footer={false} footer={false}
> >
<> <>
<div className="rb:leading-[20px] rb:mb-[8px]">{t('application.shareLink')}</div> <div className="rb:leading-5 rb:mb-2">{t('application.shareLink')}</div>
<div className="rb:mb-[12px] rb:flex rb:items-center rb:gap-[10px] rb:justify-between"> <div className="rb:mb-3 rb:flex rb:items-center rb:gap-2.5 rb:justify-between">
<div className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis rb:cursor-pointer rb:h-[32px] rb:p-[6px_10px] rb:bg-[#FFFFFF] rb:border rb:border-[#EBEBEB] rb:rounded-[6px] rb:leading-[20px]">{shareLink}</div> <div className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis rb:cursor-pointer rb:h-8 rb:p-[6px_10px] rb:bg-[#FFFFFF] rb:border rb:border-[#EBEBEB] rb:rounded-md rb:leading-5">{shareLink}</div>
<Button type="primary" loading={loading} disabled={!shareLink} onClick={handleCopy}> <Button type="primary" loading={loading} disabled={!shareLink} onClick={handleCopy}>
{t('common.copy')} {t('common.copy')}

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:51
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:51
*/
/**
* Sub-Agent Modal
* Allows adding or editing sub-agents in multi-agent cluster configuration
*/
import { forwardRef, useImperativeHandle, useState, type Key } from 'react'; import { forwardRef, useImperativeHandle, useState, type Key } from 'react';
import { Form, Select, Input } from 'antd'; import { Form, Select, Input } from 'antd';
import type { DefaultOptionType } from 'antd/es/select' import type { DefaultOptionType } from 'antd/es/select'
@@ -10,10 +21,17 @@ import { getApplicationListUrl } from '@/api/application';
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface SubAgentModalProps { interface SubAgentModalProps {
/** Callback to update sub-agent */
refresh: (agent: SubAgentItem) => void; refresh: (agent: SubAgentItem) => void;
} }
/**
* Modal for managing sub-agents
*/
const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
refresh, refresh,
}, ref) => { }, ref) => {
@@ -24,19 +42,20 @@ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
const [editVo, setEditVo] = useState<SubAgentItem>() const [editVo, setEditVo] = useState<SubAgentItem>()
const values = Form.useWatch([], form) const values = Form.useWatch([], form)
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal with optional agent data */
const handleOpen = (agent?: SubAgentItem) => { const handleOpen = (agent?: SubAgentItem) => {
setVisible(true); setVisible(true);
form.setFieldsValue(agent) form.setFieldsValue(agent)
setEditVo(agent) setEditVo(agent)
}; };
// 封装保存方法,添加提交逻辑 /** Save sub-agent configuration */
const handleSave = () => { const handleSave = () => {
form.validateFields().then(() => { form.validateFields().then(() => {
setLoading(false) setLoading(false)
@@ -47,6 +66,7 @@ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
handleClose() handleClose()
}) })
} }
/** Handle agent selection change */
const handleChange = (value: Key, option?: DefaultOptionType | DefaultOptionType[] | undefined) => { const handleChange = (value: Key, option?: DefaultOptionType | DefaultOptionType[] | undefined) => {
console.log(value, option) console.log(value, option)
if (option && !Array.isArray(option)) { if (option && !Array.isArray(option)) {
@@ -54,7 +74,7 @@ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
} }
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -73,7 +93,7 @@ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
form={form} form={form}
layout="vertical" layout="vertical"
> >
{/* Agent名称 */} {/* Agent name */}
<FormItem <FormItem
name="agent_id" name="agent_id"
label={t('application.agentName')} label={t('application.agentName')}
@@ -93,14 +113,14 @@ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
/> />
</FormItem> </FormItem>
<FormItem name="name" hidden /> <FormItem name="name" hidden />
{/* 描述 */} {/* Description */}
<FormItem <FormItem
name="role" name="role"
label={t('application.description')} label={t('application.description')}
> >
<Input.TextArea placeholder={t('common.pleaseEnter')} /> <Input.TextArea placeholder={t('common.pleaseEnter')} />
</FormItem> </FormItem>
{/* 关键词 */} {/* Keywords */}
<FormItem <FormItem
name="capabilities" name="capabilities"
label={t('application.capabilities')} label={t('application.capabilities')}

View File

@@ -1,11 +1,31 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:17
*/
/**
* Tag Component
* Displays colored status tags with different color schemes
*/
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
/**
* Tag component props
*/
export interface TagProps { export interface TagProps {
/** Tag color scheme */
color?: 'processing' | 'warning' | 'default' | 'success'; color?: 'processing' | 'warning' | 'default' | 'success';
/** Tag content */
children: ReactNode; children: ReactNode;
/** Additional CSS classes */
className?: string; className?: string;
} }
/**
* Color scheme mapping
*/
const colors = { const colors = {
processing: 'rb:text-[#155EEF] rb:border-[rgba(21,94,239,0.25)] rb:bg-[rgba(21,94,239,0.06)]', processing: 'rb:text-[#155EEF] rb:border-[rgba(21,94,239,0.25)] rb:bg-[rgba(21,94,239,0.06)]',
warning: 'rb:text-[#FF5D34] rb:border-[rgba(255,93,52,0.08)] rb:bg-[rgba(255,93,52,0.08)]', warning: 'rb:text-[#FF5D34] rb:border-[rgba(255,93,52,0.08)] rb:bg-[rgba(255,93,52,0.08)]',
@@ -13,9 +33,12 @@ const colors = {
success: 'rb:text-[#369F21] rb:border-[rgba(54,159,33,0.30)] rb:bg-[rgba(54,159,33,0.08)]', success: 'rb:text-[#369F21] rb:border-[rgba(54,159,33,0.30)] rb:bg-[rgba(54,159,33,0.08)]',
} }
/**
* Tag component for displaying status labels
*/
const Tag: FC<TagProps> = ({ color = 'processing', children, className }) => { const Tag: FC<TagProps> = ({ color = 'processing', children, className }) => {
return ( return (
<span className={`rb:inline-block rb:px-[8px] rb:py-[2px] rb:rounded-[11px] rb:text-[12px] rb:font-regular rb:leading-[16px] rb:border-[1px] ${colors[color]} ${className || ''}`}> <span className={`rb:inline-block rb:px-2 rb:py-0.5 rb:rounded-[11px] rb:text-[12px] rb:font-regular rb:leading-4 rb:border ${colors[color]} ${className || ''}`}>
{children} {children}
</span> </span>
) )

View File

@@ -1,6 +1,19 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:03
*/
/**
* Tool List Component
* Manages tool configurations for the application
* Allows adding, removing, and enabling/disabling tools
*/
import { type FC, useRef, useState, useEffect } from 'react' import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Space, Button, List, Switch } from 'antd' import { Space, Button, List, Switch } from 'antd'
import Card from '../Card' import Card from '../Card'
import type { import type {
ToolModalRef, ToolModalRef,
@@ -10,6 +23,11 @@ import Empty from '@/components/Empty'
import ToolModal from './ToolModal' import ToolModal from './ToolModal'
import { getToolMethods, getToolDetail } from '@/api/tools' import { getToolMethods, getToolDetail } from '@/api/tools'
/**
* Tool list management component
* @param value - Current tool configurations
* @param onChange - Callback when tools change
*/
const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) => void}> = ({value, onChange}) => { const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) => void}> = ({value, onChange}) => {
const { t } = useTranslation() const { t } = useTranslation()
const toolModalRef = useRef<ToolModalRef>(null) const toolModalRef = useRef<ToolModalRef>(null)
@@ -79,19 +97,23 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
} }
}, [value]) }, [value])
/** Open tool selection modal */
const handleAddTool = () => { const handleAddTool = () => {
toolModalRef.current?.handleOpen() toolModalRef.current?.handleOpen()
} }
/** Add new tool to list */
const updateTools = (tool: ToolOption) => { const updateTools = (tool: ToolOption) => {
const list = [...toolList, tool] const list = [...toolList, tool]
setToolList(list) setToolList(list)
onChange && onChange(list) onChange && onChange(list)
} }
/** Remove tool from list */
const handleDeleteTool = (index: number) => { const handleDeleteTool = (index: number) => {
const list = toolList.filter((_item, idx) => idx !== index) const list = toolList.filter((_item, idx) => idx !== index)
setToolList([...list]) setToolList([...list])
onChange && onChange(list) onChange && onChange(list)
} }
/** Toggle tool enabled state */
const handleChangeEnabled = (index: number) => { const handleChangeEnabled = (index: number) => {
const list = toolList.map((item, idx) => { const list = toolList.map((item, idx) => {
if (idx === index) { if (idx === index) {

View File

@@ -1,18 +1,37 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:06
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:06
*/
/**
* Tool Selection Modal
* Provides cascading selection of tools by type, tool, and method
* Supports MCP, builtin, and custom tool types
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Cascader, type CascaderProps } from 'antd'; import { Form, Cascader, type CascaderProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ToolModalRef, ToolOption } from '../types' import type { ToolModalRef, ToolOption } from './types'
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import { getToolMethods, getTools } from '@/api/tools' import { getToolMethods, getTools } from '@/api/tools'
import type { ToolType, ToolItem } from '@/views/ToolManagement/types' import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface ToolModalProps { interface ToolModalProps {
/** Callback to add selected tool */
refresh: (tool: ToolOption) => void; refresh: (tool: ToolOption) => void;
} }
/**
* Modal for selecting tools
*/
const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({ const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
refresh, refresh,
}, ref) => { }, ref) => {
@@ -27,7 +46,7 @@ const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
]) ])
const [selectdTools, setSelectedTools] = useState<ToolOption[]>([]) const [selectdTools, setSelectedTools] = useState<ToolOption[]>([])
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
@@ -35,12 +54,13 @@ const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
setSelectedTools([]) setSelectedTools([])
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
setVisible(true); setVisible(true);
form.resetFields(); form.resetFields();
setSelectedTools([]) setSelectedTools([])
}; };
// 封装保存方法,添加提交逻辑 /** Save selected tool */
const handleSave = () => { const handleSave = () => {
form.validateFields().then(() => { form.validateFields().then(() => {
setLoading(false) setLoading(false)
@@ -64,6 +84,7 @@ const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
handleClose() handleClose()
}) })
} }
/** Load cascader options dynamically */
const loadData = (selectedOptions: ToolOption[]) => { const loadData = (selectedOptions: ToolOption[]) => {
const targetOption = selectedOptions[selectedOptions.length - 1]; const targetOption = selectedOptions[selectedOptions.length - 1];
if (selectedOptions.length === 1) { if (selectedOptions.length === 1) {
@@ -98,12 +119,13 @@ const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
} }
}; };
/** Handle cascader selection change */
const handleChange: CascaderProps<ToolOption>['onChange'] = (_value, selectedOptions) => { const handleChange: CascaderProps<ToolOption>['onChange'] = (_value, selectedOptions) => {
console.log('selectedOptions', selectedOptions) console.log('selectedOptions', selectedOptions)
setSelectedTools(selectedOptions) setSelectedTools(selectedOptions)
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,26 +1,67 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:10
*/
/**
* Type definitions for tool configuration in application settings
*/
/**
* Tool option for cascader selection
*/
export interface ToolOption { export interface ToolOption {
/** Option value */
value?: string | number | null; value?: string | number | null;
/** Display label */
label?: React.ReactNode; label?: React.ReactNode;
/** Tool description */
description?: string; description?: string;
/** Child options for nested selection */
children?: ToolOption[]; children?: ToolOption[];
/** Whether this is a leaf node */
isLeaf?: boolean; isLeaf?: boolean;
/** Method ID for API operations */
method_id?: string; method_id?: string;
/** Operation name */
operation?: string; operation?: string;
/** Method parameters */
parameters?: Parameter[]; parameters?: Parameter[];
/** Tool ID */
tool_id?: string; tool_id?: string;
/** Whether tool is enabled */
enabled?: boolean; enabled?: boolean;
} }
/**
* Parameter definition for tool methods
*/
export interface Parameter { export interface Parameter {
/** Parameter name */
name: string; name: string;
/** Parameter data type */
type: string; type: string;
/** Parameter description */
description: string; description: string;
/** Whether parameter is required */
required: boolean; required: boolean;
/** Default value */
default: any; default: any;
/** Enum values if applicable */
enum: null | string[]; enum: null | string[];
/** Minimum value for numeric types */
minimum: number; minimum: number;
/** Maximum value for numeric types */
maximum: number; maximum: number;
/** Regex pattern for validation */
pattern: null | string; pattern: null | string;
} }
/**
* Modal ref for tool selection
*/
export interface ToolModalRef { export interface ToolModalRef {
/** Open tool selection modal */
handleOpen: () => void; handleOpen: () => void;
} }

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:18
*/
/**
* API Extension Modal
* Allows configuring external API endpoints for dynamic variable options
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -7,10 +18,17 @@ import RbModal from '@/components/RbModal'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface ApiExtensionModalProps { interface ApiExtensionModalProps {
/** Callback to refresh API extension list */
refresh: () => void; refresh: () => void;
} }
/**
* Modal for adding API extensions
*/
const ApiExtensionModal = forwardRef<ApiExtensionModalRef, ApiExtensionModalProps>(({ const ApiExtensionModal = forwardRef<ApiExtensionModalRef, ApiExtensionModalProps>(({
refresh refresh
}, ref) => { }, ref) => {
@@ -21,18 +39,19 @@ const ApiExtensionModal = forwardRef<ApiExtensionModalRef, ApiExtensionModalProp
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false) setLoading(false)
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
form.resetFields(); form.resetFields();
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save API extension configuration */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -46,7 +65,7 @@ const ApiExtensionModal = forwardRef<ApiExtensionModalRef, ApiExtensionModalProp
}); });
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:27
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:27
*/
/**
* Variable Edit Modal
* Allows creating and editing application input variables
* Supports multiple variable types: text, paragraph, number, dropdown, checkbox, API variable
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Form, Input, Select, InputNumber, Checkbox, Tag, Divider, Button } from 'antd'; import { Form, Input, Select, InputNumber, Checkbox, Tag, Divider, Button } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,10 +21,17 @@ import ApiExtensionModal from './ApiExtensionModal'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface VariableEditModalProps { interface VariableEditModalProps {
/** Callback to update variable */
refreshTable: (values: Variable) => void; refreshTable: (values: Variable) => void;
} }
/**
* Supported variable types
*/
const types = [ const types = [
'text', 'text',
'paragraph', 'paragraph',
@@ -21,6 +40,9 @@ const types = [
// 'checkbox', // 'checkbox',
// 'apiVariable' // 'apiVariable'
] ]
/**
* Variable type to data type mapping
*/
const variableType = { const variableType = {
text: 'string', text: 'string',
paragraph: 'string', paragraph: 'string',
@@ -30,6 +52,9 @@ const variableType = {
apiVariable: 'string', apiVariable: 'string',
} }
/**
* Modal for editing variables
*/
const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProps>(({ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProps>(({
refreshTable refreshTable
}, ref) => { }, ref) => {
@@ -42,7 +67,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
@@ -50,6 +75,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
setEditVo(null) setEditVo(null)
}; };
/** Open modal with optional variable data */
const handleOpen = (variable?: Variable) => { const handleOpen = (variable?: Variable) => {
setVisible(true); setVisible(true);
if (variable) { if (variable) {
@@ -59,7 +85,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
form.resetFields(); form.resetFields();
} }
}; };
// 封装保存方法,添加提交逻辑 /** Save variable configuration */
const handleSave = () => { const handleSave = () => {
form.validateFields().then((values) => { form.validateFields().then((values) => {
refreshTable({ refreshTable({
@@ -70,12 +96,12 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
}) })
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
})); }));
// 变量类型改变时,更新初始化其他字段值 /** Handle variable type change */
const handleChangeType = (value: Variable['type']) => { const handleChangeType = (value: Variable['type']) => {
if (value) { if (value) {
form.setFieldsValue({ form.setFieldsValue({
@@ -90,7 +116,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
}) })
} }
} }
// 添加 API 扩展 /** Add API extension */
const addApiExtension = () => { const addApiExtension = () => {
apiExtensionModalRef.current?.handleOpen() apiExtensionModalRef.current?.handleOpen()
} }
@@ -111,7 +137,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
layout="vertical" layout="vertical"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }} scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
> >
{/* 变量类型 */} {/* Variable Type */}
<FormItem <FormItem
name="type" name="type"
label={t('application.variableType')} label={t('application.variableType')}
@@ -128,7 +154,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
optionRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>} optionRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
/> />
</FormItem> </FormItem>
{/* 变量名称 */} {/* Variable Name */}
<FormItem <FormItem
name="name" name="name"
label={t('application.variableName')} label={t('application.variableName')}
@@ -146,7 +172,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
}} }}
/> />
</FormItem> </FormItem>
{/* 显示名称 */} {/* Display Name */}
<FormItem <FormItem
name="display_name" name="display_name"
label={t('application.displayName')} label={t('application.displayName')}
@@ -154,14 +180,14 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
> >
<Input placeholder={t('common.enter')} /> <Input placeholder={t('common.enter')} />
</FormItem> </FormItem>
{/* 描述 */} {/* Description */}
<FormItem <FormItem
name="description" name="description"
label={t('application.description')} label={t('application.description')}
> >
<Input placeholder={t('common.enter')} /> <Input placeholder={t('common.enter')} />
</FormItem> </FormItem>
{/* 最大长度 */} {/* Max Length */}
{['text', 'paragraph'].includes(values?.type) && ( {['text', 'paragraph'].includes(values?.type) && (
<FormItem <FormItem
name="max_length" name="max_length"
@@ -170,7 +196,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
<InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} /> <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
</FormItem> </FormItem>
)} )}
{/* 默认值 */} {/* Default Value */}
{/* {['text', 'paragraph', 'number', 'checkbox'].includes(values?.type) && ( {/* {['text', 'paragraph', 'number', 'checkbox'].includes(values?.type) && (
<FormItem <FormItem
name="default_value" name="default_value"
@@ -182,7 +208,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
{['checkbox'].includes(values.type) && <Select options={[{ value: true, label: t('application.defaultChecked') }, { value: false, label: t('application.notDefaultChecked') }]} />} {['checkbox'].includes(values.type) && <Select options={[{ value: true, label: t('application.defaultChecked') }, { value: false, label: t('application.notDefaultChecked') }]} />}
</FormItem> </FormItem>
)} */} )} */}
{/* 选项 */} {/* Options */}
{['dropdownOptions'].includes(values?.type) && ( {['dropdownOptions'].includes(values?.type) && (
<FormItem <FormItem
name="options" name="options"
@@ -191,7 +217,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
<SortableList /> <SortableList />
</FormItem> </FormItem>
)} )}
{/* API 扩展 */} {/* API Extension */}
{['apiVariable'].includes(values?.type) && ( {['apiVariable'].includes(values?.type) && (
<FormItem <FormItem
name="api_extension" name="api_extension"
@@ -211,14 +237,14 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
/> />
</FormItem> </FormItem>
)} )}
{/* 是否必填 */} {/* Required */}
<FormItem <FormItem
name="required" name="required"
valuePropName="checked" valuePropName="checked"
> >
<Checkbox>{t('application.required')}</Checkbox> <Checkbox>{t('application.required')}</Checkbox>
</FormItem> </FormItem>
{/* 是否隐藏 */} {/* Hidden */}
{/* <FormItem {/* <FormItem
name="hidden" name="hidden"
valuePropName="checked" valuePropName="checked"

View File

@@ -1,6 +1,19 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:32
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:32
*/
/**
* Variable List Component
* Manages application input variables configuration
* Allows adding, editing, and removing variables
*/
import { type FC, useRef } from 'react' import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Space, Button, Switch, Form } from 'antd' import { Space, Button, Switch, Form } from 'antd'
import variablesEmpty from '@/assets/images/application/variablesEmpty.svg' import variablesEmpty from '@/assets/images/application/variablesEmpty.svg'
import Card from '../Card' import Card from '../Card'
import Table from '@/components/Table'; import Table from '@/components/Table';
@@ -8,17 +21,27 @@ import type { Variable, VariableEditModalRef } from './types'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import VariableEditModal from './VariableEditModal' import VariableEditModal from './VariableEditModal'
/**
* Component props
*/
interface VariableListProps { interface VariableListProps {
/** Current variable list */
value?: Variable[]; value?: Variable[];
/** Callback when variables change */
onChange?: (value: Variable[]) => void; onChange?: (value: Variable[]) => void;
} }
const VariableList: FC<VariableListProps> = ({value = [], onChange}) => {
/**
* Variable list management component
*/const VariableList: FC<VariableListProps> = ({value = [], onChange}) => {
const { t } = useTranslation() const { t } = useTranslation()
const variableEditModalRef = useRef<VariableEditModalRef>(null) const variableEditModalRef = useRef<VariableEditModalRef>(null)
/** Open variable edit modal */
const handleAddVariable = () => { const handleAddVariable = () => {
variableEditModalRef.current?.handleOpen() variableEditModalRef.current?.handleOpen()
} }
/** Save variable changes */
const handleSaveVariable = (variable: Variable) => { const handleSaveVariable = (variable: Variable) => {
const newList = [...(value || [])] const newList = [...(value || [])]
if (variable.index !== undefined && variable.index >= 0) { if (variable.index !== undefined && variable.index >= 0) {

View File

@@ -1,28 +1,69 @@
export interface Variable { /*
index?: number; * @Author: ZhaoYing
name: string; * @Date: 2026-02-03 16:26:21
display_name: string; * @Last Modified by: ZhaoYing
type: string; * @Last Modified time: 2026-02-03 16:26:21
required: boolean; */
max_length?: number; /**
description?: string; * Type definitions for variable configuration in application settings
*/
/**
* Variable definition for application input
*/
export interface Variable {
/** Variable index in list */
index?: number;
/** Variable name (identifier) */
name: string;
/** Display name shown to users */
display_name: string;
/** Variable data type (string, number, select, etc.) */
type: string;
/** Whether variable is required */
required: boolean;
/** Maximum length for string types */
max_length?: number;
/** Variable description */
description?: string;
/** Unique key for React rendering */
key?: string; key?: string;
/** Default value */
default_value?: string; default_value?: string;
/** Options for select type variables */
options?: string[]; options?: string[];
/** API extension for dynamic options */
api_extension?: string; api_extension?: string;
/** Whether variable is hidden from UI */
hidden?: boolean; hidden?: boolean;
/** Current value */
value?: any; value?: any;
} }
/**
* Modal ref for variable editing
*/
export interface VariableEditModalRef { export interface VariableEditModalRef {
/** Open modal with optional existing variable data */
handleOpen: (values?: Variable) => void; handleOpen: (values?: Variable) => void;
} }
/**
* API extension configuration data
*/
export interface ApiExtensionModalData { export interface ApiExtensionModalData {
/** Extension name */
name: string; name: string;
/** API endpoint URL */
apiEndpoint: string; apiEndpoint: string;
/** API authentication key */
apiKey: string; apiKey: string;
} }
/**
* Modal ref for API extension configuration
*/
export interface ApiExtensionModalRef { export interface ApiExtensionModalRef {
/** Open API extension modal */
handleOpen: () => void; handleOpen: () => void;
} }

View File

@@ -1,5 +1,12 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:37
*/
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ConfigHeader from './components/ConfigHeader' import ConfigHeader from './components/ConfigHeader'
import type { AgentRef, ClusterRef, WorkflowRef } from './types' import type { AgentRef, ClusterRef, WorkflowRef } from './types'
import type { Application } from '@/views/ApplicationManagement/types' import type { Application } from '@/views/ApplicationManagement/types'
@@ -11,14 +18,28 @@ import { getApplication } from '@/api/application'
import Workflow from '@/views/Workflow'; import Workflow from '@/views/Workflow';
import Statistics from './Statistics' import Statistics from './Statistics'
/**
* Application configuration page component
* Main container for configuring agents, workflows, multi-agent clusters
* Manages tabs for arrangement, API, release, and statistics
*/
const ApplicationConfig: React.FC = () => { const ApplicationConfig: React.FC = () => {
// Hooks
const { id } = useParams(); const { id } = useParams();
// Refs for different application types
const agentRef = useRef<AgentRef>(null) const agentRef = useRef<AgentRef>(null)
const clusterRef = useRef<ClusterRef>(null) const clusterRef = useRef<ClusterRef>(null)
const workflowRef = useRef<WorkflowRef>(null) const workflowRef = useRef<WorkflowRef>(null)
// State
const [application, setApplication] = useState<Application | null>(null); const [application, setApplication] = useState<Application | null>(null);
const [activeTab, setActiveTab] = useState('arrangement'); const [activeTab, setActiveTab] = useState('arrangement');
/**
* Handle tab change with auto-save for arrangement tab
* @param key - New tab key
*/
const handleChangeTab = async (key: string) => { const handleChangeTab = async (key: string) => {
if (activeTab === 'arrangement' && application?.type === 'agent' && agentRef.current) { if (activeTab === 'arrangement' && application?.type === 'agent' && agentRef.current) {
agentRef.current.handleSave(false) agentRef.current.handleSave(false)
@@ -44,6 +65,9 @@ const ApplicationConfig: React.FC = () => {
getApplicationInfo() getApplicationInfo()
}, [id]) }, [id])
/**
* Fetch application information
*/
const getApplicationInfo = () => { const getApplicationInfo = () => {
if (!id) { if (!id) {
return return

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:49
*/
import type { KnowledgeConfig } from './components/Knowledge/types' import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Variable } from './components/VariableList/types' import type { Variable } from './components/VariableList/types'
import type { ToolOption } from './components/ToolList/types' import type { ToolOption } from './components/ToolList/types'
@@ -5,164 +11,389 @@ import type { ChatItem } from '@/components/Chat/types'
import type { GraphRef } from '@/views/Workflow/types'; import type { GraphRef } from '@/views/Workflow/types';
import type { ApiKey } from '@/views/ApiKeyManagement/types' import type { ApiKey } from '@/views/ApiKeyManagement/types'
/**
* Model configuration parameters
*/
export interface ModelConfig { export interface ModelConfig {
/** Model label */
label?: string; label?: string;
/** Default model configuration ID */
default_model_config_id?: string; default_model_config_id?: string;
/** Temperature for response randomness (0-2) */
temperature: number; temperature: number;
/** Maximum tokens in response */
max_tokens: number; max_tokens: number;
/** Top-p sampling parameter */
top_p: number; top_p: number;
/** Frequency penalty */
frequency_penalty: number; frequency_penalty: number;
/** Presence penalty */
presence_penalty: number; presence_penalty: number;
/** Number of completions to generate */
n: number; n: number;
/** Stop sequences */
stop?: string; stop?: string;
} }
/**
* Memory configuration
*/
export interface MemoryConfig { export interface MemoryConfig {
/** Whether memory is enabled */
enabled: boolean; enabled: boolean;
/** Memory content */
memory_content?: string; memory_content?: string;
/** Maximum history length */
max_history?: number | string; max_history?: number | string;
} }
/**
* Application configuration
*/
export interface Config extends MultiAgentConfig { export interface Config extends MultiAgentConfig {
/** Configuration ID */
id: string; id: string;
/** Application ID */
app_id: string; app_id: string;
/** System prompt */
system_prompt: string; system_prompt: string;
/** Default model configuration ID */
default_model_config_id?: string; default_model_config_id?: string;
/** Model parameters */
model_parameters: ModelConfig; model_parameters: ModelConfig;
/** Knowledge retrieval configuration */
knowledge_retrieval: KnowledgeConfig | null; knowledge_retrieval: KnowledgeConfig | null;
/** Memory configuration */
memory?: MemoryConfig; memory?: MemoryConfig;
/** Variables list */
variables: Variable[]; variables: Variable[];
/** Tools list */
tools: ToolOption[]; tools: ToolOption[];
/** Whether configuration is active */
is_active: boolean; is_active: boolean;
/** Creation timestamp */
created_at: number; created_at: number;
/** Last update timestamp */
updated_at: number; updated_at: number;
} }
/**
* Multi-agent configuration
*/
export interface MultiAgentConfig { export interface MultiAgentConfig {
/** Configuration ID */
id: string; id: string;
/** Application ID */
app_id: string; app_id: string;
/** Default model configuration ID */
default_model_config_id?: string; default_model_config_id?: string;
/** Model parameters */
model_parameters: ModelConfig; model_parameters: ModelConfig;
/** Sub-agents list */
sub_agents?: SubAgentItem[]; sub_agents?: SubAgentItem[];
/** Routing rules */
routing_rules: null; routing_rules: null;
/** Orchestration mode */
orchestration_mode: 'supervisor' | 'collaboration'; orchestration_mode: 'supervisor' | 'collaboration';
/** Execution configuration */
execution_config: { execution_config: {
/** Sub-agent execution mode */
sub_agent_execution_mode: 'sequential' | 'parallel'; sub_agent_execution_mode: 'sequential' | 'parallel';
}; };
/** Aggregation strategy */
aggregation_strategy: 'merge' | 'vote' | 'priority' aggregation_strategy: 'merge' | 'vote' | 'priority'
} }
// 创建表单数据类型 /**
* Application modal form data
*/
export interface ApplicationModalData { export interface ApplicationModalData {
/** Application name */
name: string; name: string;
/** Application type */
type: string; type: string;
/** Application icon */
icon: string; icon: string;
} }
// 定义组件暴露的方法接口 /**
* Agent component ref methods
*/
export interface AgentRef { export interface AgentRef {
/**
* Save agent configuration
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>; handleSave: (flag?: boolean) => Promise<unknown>;
} }
/**
* Cluster component ref methods
*/
export interface ClusterRef { export interface ClusterRef {
/**
* Save cluster configuration
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>; handleSave: (flag?: boolean) => Promise<unknown>;
} }
/**
* Workflow component ref methods
*/
export interface WorkflowRef { export interface WorkflowRef {
/**
* Save workflow configuration
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>; handleSave: (flag?: boolean) => Promise<unknown>;
/** Run workflow */
handleRun: () => void; handleRun: () => void;
/** Graph reference */
graphRef: GraphRef; graphRef: GraphRef;
/** Add variable */
addVariable: () => void; addVariable: () => void;
} }
/**
* Application modal ref methods
*/
export interface ApplicationModalRef { export interface ApplicationModalRef {
/**
* Open application modal
* @param application - Optional application data for edit mode
*/
handleOpen: (application?: Config) => void; handleOpen: (application?: Config) => void;
} }
/**
* Model configuration source type
*/
export type Source = 'chat' | 'model' | 'multi_agent' export type Source = 'chat' | 'model' | 'multi_agent'
/**
* Model configuration modal ref methods
*/
export interface ModelConfigModalRef { export interface ModelConfigModalRef {
/**
* Open model configuration modal
* @param source - Configuration source
* @param model - Optional model data
*/
handleOpen: (source: Source, model?: any) => void; handleOpen: (source: Source, model?: any) => void;
} }
/**
* Model configuration modal form data
*/
export interface ModelConfigModalData { export interface ModelConfigModalData {
/** Model identifier */
model: string; model: string;
/** Additional configuration fields */
[key: string]: string; [key: string]: string;
} }
/**
* AI prompt modal ref methods
*/
export interface AiPromptModalRef { export interface AiPromptModalRef {
handleOpen: () => void; /** Open AI prompt modal */
}
export interface ChatData {
label?: string;
model_config_id?: string;
model_parameters?: ModelConfig;
list?: ChatItem[];
conversation_id?: string | null;
}
export interface Release {
id: string;
app_id: string;
version: string;
release_notes: string;
name: string;
description?: string;
icon: string;
icon_type?: string;
type: string;
visibility: string;
config: Config;
default_model_config_id?: string;
published_by?: string;
published_at: number;
publisher_name?: string;
is_active?: boolean;
created_at?: number;
updated_at?: number;
status?: string;
version_name?: string;
tagKey: 'current' | 'rolledBack' | 'history';
}
export interface ReleaseModalRef {
handleOpen: () => void;
}
export interface ReleaseShareModalRef {
handleOpen: () => void;
}
export interface CopyModalRef {
handleOpen: () => void;
}
export interface SubAgentItem {
agent_id: string;
name: string;
role: string;
capabilities: string[];
is_active?: boolean;
}
export interface SubAgentModalRef {
handleOpen: (agent?: SubAgentItem) => void;
}
export interface ApiKeyModalRef {
handleOpen: () => void;
}
export interface ApiKeyConfigModalRef {
handleOpen: (apiKey: ApiKey) => void;
}
export interface AiPromptVariableModalRef {
handleOpen: () => void; handleOpen: () => void;
} }
/**
* Chat data structure
*/
export interface ChatData {
/** Chat label */
label?: string;
/** Model configuration ID */
model_config_id?: string;
/** Model parameters */
model_parameters?: ModelConfig;
/** Chat messages list */
list?: ChatItem[];
/** Conversation ID */
conversation_id?: string | null;
}
/**
* Release version data
*/
export interface Release {
/** Release ID */
id: string;
/** Application ID */
app_id: string;
/** Version number */
version: string;
/** Release notes */
release_notes: string;
/** Release name */
name: string;
/** Release description */
description?: string;
/** Application icon */
icon: string;
/** Icon type */
icon_type?: string;
/** Application type */
type: string;
/** Visibility setting */
visibility: string;
/** Configuration snapshot */
config: Config;
/** Default model configuration ID */
default_model_config_id?: string;
/** Publisher user ID */
published_by?: string;
/** Publication timestamp */
published_at: number;
/** Publisher name */
publisher_name?: string;
/** Whether release is active */
is_active?: boolean;
/** Creation timestamp */
created_at?: number;
/** Last update timestamp */
updated_at?: number;
/** Release status */
status?: string;
/** Version name */
version_name?: string;
/** Tag key for UI display */
tagKey: 'current' | 'rolledBack' | 'history';
}
/**
* Release modal ref methods
*/
export interface ReleaseModalRef {
/** Open release modal */
handleOpen: () => void;
}
/**
* Release share modal ref methods
*/
export interface ReleaseShareModalRef {
/** Open release share modal */
handleOpen: () => void;
}
/**
* Copy modal ref methods
*/
export interface CopyModalRef {
/** Open copy modal */
handleOpen: () => void;
}
/**
* Sub-agent item data
*/
export interface SubAgentItem {
/** Agent ID */
agent_id: string;
/** Agent name */
name: string;
/** Agent role */
role: string;
/** Agent capabilities */
capabilities: string[];
/** Whether agent is active */
is_active?: boolean;
}
/**
* Sub-agent modal ref methods
*/
export interface SubAgentModalRef {
/**
* Open sub-agent modal
* @param agent - Optional agent data for edit mode
*/
handleOpen: (agent?: SubAgentItem) => void;
}
/**
* API key modal ref methods
*/
export interface ApiKeyModalRef {
/** Open API key modal */
handleOpen: () => void;
}
/**
* API key configuration modal ref methods
*/
export interface ApiKeyConfigModalRef {
/**
* Open API key configuration modal
* @param apiKey - API key data
*/
handleOpen: (apiKey: ApiKey) => void;
}
/**
* AI prompt variable modal ref methods
*/
export interface AiPromptVariableModalRef {
/** Open AI prompt variable modal */
handleOpen: () => void;
}
/**
* AI prompt form data
*/
export interface AiPromptForm { export interface AiPromptForm {
/** Model ID */
model_id?: string; model_id?: string;
/** Message content */
message?: string; message?: string;
/** Current prompt */
current_prompt?: string; current_prompt?: string;
} }
/**
* Chat variable configuration modal ref methods
*/
export interface ChatVariableConfigModalRef { export interface ChatVariableConfigModalRef {
/**
* Open chat variable configuration modal
* @param values - Variables list
*/
handleOpen: (values: Variable[]) => void; handleOpen: (values: Variable[]) => void;
} }
/**
* Statistics item data
*/
export interface StatisticsItem { export interface StatisticsItem {
/** Count value */
count: number; count: number;
/** Date string */
date: string; date: string;
} }
/**
* Statistics data structure
*/
export interface StatisticsData { export interface StatisticsData {
/** Daily conversations statistics */
daily_conversations: StatisticsItem[]; daily_conversations: StatisticsItem[];
/** Daily new users statistics */
daily_new_users: StatisticsItem[]; daily_new_users: StatisticsItem[];
/** Daily API calls statistics */
daily_api_calls: StatisticsItem[]; daily_api_calls: StatisticsItem[];
/** Daily tokens usage statistics */
daily_tokens: StatisticsItem[]; daily_tokens: StatisticsItem[];
/** Total conversations count */
total_conversations: number; total_conversations: number;
/** Total new users count */
total_new_users: number; total_new_users: number;
/** Total API calls count */
total_api_calls: number; total_api_calls: number;
/** Total tokens used */
total_tokens: number; total_tokens: number;
} }

View File

@@ -1,32 +1,57 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:35:30
*/
/**
* Application Modal
* Modal for creating and editing applications
* Supports three application types: agent, multi_agent, and workflow
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import RadioGroupCard from '@/components/RadioGroupCard' import RadioGroupCard from '@/components/RadioGroupCard'
import AgentIcon from '@/assets/images/application/agent.svg' import AgentIcon from '@/assets/images/application/agent.svg'
import ClusterIcon from '@/assets/images/application/cluster.svg' import ClusterIcon from '@/assets/images/application/cluster.svg'
import WorkflowIcon from '@/assets/images/application/workflow.svg' import WorkflowIcon from '@/assets/images/application/workflow.svg'
import type { ApplicationModalData, ApplicationModalRef, Application } from '../types' import type { ApplicationModalData, ApplicationModalRef, Application } from '../types'
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import { addApplication, updateApplication } from '@/api/application' import { addApplication, updateApplication } from '@/api/application'
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface ApplicationModalProps { interface ApplicationModalProps {
/** Callback to refresh application list */
refresh: () => void; refresh: () => void;
} }
/**
* Supported application types
*/
const types = [ const types = [
'agent', 'agent',
'multi_agent', 'multi_agent',
'workflow' 'workflow'
] ]
/**
* Application type icon mapping
*/
const typeIcons: Record<string, string> = { const typeIcons: Record<string, string> = {
agent: AgentIcon, agent: AgentIcon,
multi_agent: ClusterIcon, multi_agent: ClusterIcon,
workflow: WorkflowIcon workflow: WorkflowIcon
} }
/**
* Modal for creating and editing applications
*/
const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(({ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(({
refresh refresh
}, ref) => { }, ref) => {
@@ -38,7 +63,7 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
@@ -46,6 +71,7 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
setEditVo(null) setEditVo(null)
}; };
/** Open modal with optional application data for editing */
const handleOpen = (application?: Application) => { const handleOpen = (application?: Application) => {
if (application) { if (application) {
setEditVo(application || null) setEditVo(application || null)
@@ -59,7 +85,7 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
} }
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save application (create or update) */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -83,7 +109,7 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
}); });
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,8 +1,21 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:34:12
*/
/**
* Application Management Page
* Displays and manages all applications in the workspace
* Supports creating, editing, and deleting applications
*/
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App } from 'antd'; import { Button, Row, Col, App } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import type { Application, ApplicationModalRef, Query } from './types'; import type { Application, ApplicationModalRef, Query } from './types';
import ApplicationModal from './components/ApplicationModal'; import ApplicationModal from './components/ApplicationModal';
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput'
@@ -11,6 +24,9 @@ import { getApplicationListUrl, deleteApplication } from '@/api/application'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
/**
* Application management main component
*/
const ApplicationManagement: React.FC = () => { const ApplicationManagement: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { modal } = App.useApp(); const { modal } = App.useApp();
@@ -18,16 +34,20 @@ const ApplicationManagement: React.FC = () => {
const applicationModalRef = useRef<ApplicationModalRef>(null); const applicationModalRef = useRef<ApplicationModalRef>(null);
const scrollListRef = useRef<PageScrollListRef>(null) const scrollListRef = useRef<PageScrollListRef>(null)
/** Refresh application list */
const refresh = () => { const refresh = () => {
scrollListRef.current?.refresh(); scrollListRef.current?.refresh();
} }
/** Open create application modal */
const handleCreate = () => { const handleCreate = () => {
applicationModalRef.current?.handleOpen(); applicationModalRef.current?.handleOpen();
} }
/** Navigate to application configuration page */
const handleEdit = (item: Application) => { const handleEdit = (item: Application) => {
window.open(`/#/application/config/${item.id}`); window.open(`/#/application/config/${item.id}`);
} }
/** Delete application with confirmation */
const handleDelete = (item: Application) => { const handleDelete = (item: Application) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.name }), title: t('common.confirmDeleteDesc', { name: item.name }),

View File

@@ -1,76 +1,174 @@
// 应用数据类型 /*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:34:15
*/
/**
* Type definitions for Application Management
*/
/**
* Search query parameters
*/
export interface Query { export interface Query {
/** Search keyword */
search: string; search: string;
} }
/**
* Application data structure
*/
export interface Application { export interface Application {
/** Application ID */
id: string; id: string;
/** Workspace ID */
workspace_id: string; workspace_id: string;
/** Creator user ID */
created_by: string; created_by: string;
/** Application name */
name: string; name: string;
/** Application description */
description?: string; description?: string;
/** Icon URL */
icon?: string; icon?: string;
/** Icon type */
icon_type?: string; icon_type?: string;
/** Application type: agent, multi_agent, or workflow */
type: 'agent' | 'multi_agent' | 'workflow'; type: 'agent' | 'multi_agent' | 'workflow';
/** Visibility setting */
visibility: string; visibility: string;
/** Application status */
status: string; status: string;
/** Application tags */
tags: string[]; tags: string[];
/** Current release version ID */
current_release_id?: string; current_release_id?: string;
/** Whether application is active */
is_active: boolean; is_active: boolean;
/** Whether application is shared */
is_shared: boolean; is_shared: boolean;
/** Creation timestamp */
created_at: number; created_at: number;
/** Last update timestamp */
updated_at: number; updated_at: number;
} }
// 创建表单数据类型 /**
* Application creation/edit form data
*/
export interface ApplicationModalData { export interface ApplicationModalData {
/** Application name */
name: string; name: string;
/** Application type */
type: string; type: string;
/** Application description */
description?: string; description?: string;
/** Icon upload data */
icon: { icon: {
url: string; url: string;
uid: string | number; uid: string | number;
}[]; }[];
} }
// 定义组件暴露的方法接口 /**
* Application modal ref interface
*/
export interface ApplicationModalRef { export interface ApplicationModalRef {
/** Open modal with optional application data for editing */
handleOpen: (application?: Application) => void; handleOpen: (application?: Application) => void;
} }
/**
* Model configuration modal ref interface
*/
export interface ModelConfigModalRef { export interface ModelConfigModalRef {
/** Open modal with application data */
handleOpen: (application?: Application) => void; handleOpen: (application?: Application) => void;
} }
/**
* Model configuration form data
*/
export interface ModelConfigModalData { export interface ModelConfigModalData {
/** Selected model */
model: string; model: string;
/** Additional configuration fields */
[key: string]: string; [key: string]: string;
} }
/**
* AI prompt modal ref interface
*/
export interface AiPromptModalRef { export interface AiPromptModalRef {
/** Open modal with application data */
handleOpen: (application?: Application) => void; handleOpen: (application?: Application) => void;
} }
/**
* Variable modal ref interface
*/
export interface VariableModalRef { export interface VariableModalRef {
/** Open modal with application data */
handleOpen: (application?: Application) => void; handleOpen: (application?: Application) => void;
} }
/**
* Variable modal props
*/
export interface VariableModalProps { export interface VariableModalProps {
/** Callback to refresh variable list */
refresh: () => void; refresh: () => void;
} }
/**
* Variable edit modal ref interface
*/
export interface VariableEditModalRef { export interface VariableEditModalRef {
/** Open modal with optional variable data */
handleOpen: (values?: Variable) => void; handleOpen: (values?: Variable) => void;
} }
/**
* Variable data structure
*/
export interface Variable { export interface Variable {
/** Variable index */
index?: number; index?: number;
/** Variable type */
type: string; type: string;
/** Variable key */
key: string; key: string;
/** Variable name */
name: string; name: string;
/** Maximum length for string types */
maxLength?: number; maxLength?: number;
/** Default value */
defaultValue?: string; defaultValue?: string;
/** Options for select type */
options?: string[]; options?: string[];
/** Whether variable is required */
required: boolean; required: boolean;
/** Whether variable is hidden */
hidden?: boolean; hidden?: boolean;
} }
/**
* API extension configuration data
*/
export interface ApiExtensionModalData { export interface ApiExtensionModalData {
/** Extension name */
name: string; name: string;
/** API endpoint URL */
apiEndpoint: string; apiEndpoint: string;
/** API authentication key */
apiKey: string; apiKey: string;
} }
/**
* API extension modal ref interface
*/
export interface ApiExtensionModalRef { export interface ApiExtensionModalRef {
/** Open API extension modal */
handleOpen: () => void; handleOpen: () => void;
} }

View File

@@ -1,10 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:58:35
*/
/**
* Conversation Page
* Public conversation interface for shared applications
* Supports conversation history, streaming responses, and memory/web search features
*/
import { type FC, useState, useEffect, useRef } from 'react' import { type FC, useState, useEffect, useRef } from 'react'
import { useParams, useLocation } from 'react-router-dom' import { useParams, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScroll from 'react-infinite-scroll-component';
import { Flex, Skeleton, Form } from 'antd' import { Flex, Skeleton, Form } from 'antd'
import clsx from 'clsx' import clsx from 'clsx'
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.svg' import dayjs from 'dayjs'
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application' import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
import type { HistoryItem, QueryParams } from './types' import type { HistoryItem, QueryParams } from './types'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
@@ -19,9 +32,11 @@ import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
import OnlineIcon from '@/assets/images/conversation/online.svg' import OnlineIcon from '@/assets/images/conversation/online.svg'
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg' import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg' import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
import dayjs from 'dayjs'
import { type SSEMessage } from '@/utils/stream' import { type SSEMessage } from '@/utils/stream'
/**
* Conversation component for shared applications
*/
const Conversation: FC = () => { const Conversation: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { token } = useParams() const { token } = useParams()
@@ -61,7 +76,7 @@ const Conversation: FC = () => {
} }
}, [token, shareToken, page, hasMore, historyList]) }, [token, shareToken, page, hasMore, historyList])
// 按日期分组历史记录 /** Group conversation history by date */
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => { const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => {
return items.reduce((groups: Record<string, HistoryItem[]>, item) => { return items.reduce((groups: Record<string, HistoryItem[]>, item) => {
const date = formatDateTime(item.created_at, 'YYYY-MM-DD') const date = formatDateTime(item.created_at, 'YYYY-MM-DD')
@@ -74,6 +89,7 @@ const Conversation: FC = () => {
}, {}); }, {});
} }
/** Fetch conversation history with pagination */
const getHistory = (flag: boolean = false) => { const getHistory = (flag: boolean = false) => {
if (!token || (pageLoading || !hasMore) && !flag) { if (!token || (pageLoading || !hasMore) && !flag) {
return return
@@ -104,6 +120,7 @@ const Conversation: FC = () => {
setPageLoading(false); setPageLoading(false);
}) })
} }
/** Switch to different conversation or start new one */
const handleChangeHistory = (id: string | null) => { const handleChangeHistory = (id: string | null) => {
if (id !== conversation_id) { if (id !== conversation_id) {
setConversationId(id) setConversationId(id)
@@ -124,6 +141,7 @@ const Conversation: FC = () => {
} }
}, [conversation_id]) }, [conversation_id])
/** Add user message to chat */
const addUserMessage = (message: string = '') => { const addUserMessage = (message: string = '') => {
const newUserMessage: ChatItem = { const newUserMessage: ChatItem = {
conversation_id, conversation_id,
@@ -133,6 +151,7 @@ const Conversation: FC = () => {
}; };
setChatList(prev => [...prev, newUserMessage]) setChatList(prev => [...prev, newUserMessage])
} }
/** Add empty assistant message placeholder */
const addAssistantMessage = () => { const addAssistantMessage = () => {
const newAssistantMessage: ChatItem = { const newAssistantMessage: ChatItem = {
created_at: Date.now(), created_at: Date.now(),
@@ -141,6 +160,7 @@ const Conversation: FC = () => {
} }
setChatList(prev => [...prev, newAssistantMessage]) setChatList(prev => [...prev, newAssistantMessage])
} }
/** Update assistant message with streaming content */
const updateAssistantMessage = (content: string = '') => { const updateAssistantMessage = (content: string = '') => {
if (!content) return if (!content) return
if (streamLoading) { if (streamLoading) {
@@ -164,6 +184,7 @@ const Conversation: FC = () => {
}) })
} }
/** Send message and handle streaming response */
const handleSend = () => { const handleSend = () => {
if (!token || !shareToken) { if (!token || !shareToken) {
return return
@@ -261,7 +282,7 @@ const Conversation: FC = () => {
</div> </div>
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]"> <div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
<div className='rb:w-[760px] rb:h-screen rb:mx-auto rb:pt-10'> <div className='rb:w-190 rb:h-screen rb:mx-auto rb:pt-10'>
<Chat <Chat
empty={<Empty url={ChatEmpty} className="rb:h-full" size={[320,180]} title={t('memoryConversation.chatEmpty')} subTitle={t('memoryConversation.emptyDesc')} />} empty={<Empty url={ChatEmpty} className="rb:h-full" size={[320,180]} title={t('memoryConversation.chatEmpty')} subTitle={t('memoryConversation.emptyDesc')} />}
contentClassName="rb:h-[calc(100%-152px)] " contentClassName="rb:h-[calc(100%-152px)] "

View File

@@ -1,21 +1,53 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:57:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:57:46
*/
/**
* Type definitions for Conversation
*/
/**
* Conversation history item
*/
export interface HistoryItem { export interface HistoryItem {
/** Conversation ID */
id: string; id: string;
/** Application ID */
app_id: string; app_id: string;
/** Workspace ID */
workspace_id: string; workspace_id: string;
/** User ID */
user_id: string | null; user_id: string | null;
/** Conversation title */
title: string; title: string;
/** Conversation summary */
summary?: string summary?: string
/** Whether conversation is draft */
is_draft: boolean; is_draft: boolean;
/** Number of messages */
message_count: number; message_count: number;
/** Whether conversation is active */
is_active: boolean; is_active: boolean;
/** Creation timestamp */
created_at: number; created_at: number;
/** Update timestamp */
updated_at: number; updated_at: number;
} }
/**
* Query parameters for sending messages
*/
export interface QueryParams { export interface QueryParams {
/** Message content */
message?: string; message?: string;
/** Whether to enable web search */
web_search?: boolean; web_search?: boolean;
/** Whether to enable memory */
memory?: boolean; memory?: boolean;
/** Whether to use streaming response */
stream: boolean; stream: boolean;
/** Current conversation ID */
conversation_id?: string | null; conversation_id?: string | null;
} }

View File

@@ -1,7 +1,20 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:56:54
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:57:17
*/
/**
* Emotion Engine Configuration Page
* Configures emotion analysis settings for memory system
* Includes model selection, intensity threshold, and feature toggles
*/
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Row, Col, Form, Slider, Button, Alert, message, Space } from 'antd'; import { Row, Col, Form, Slider, Button, Alert, message, Space } from 'antd';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card'; import RbCard from '@/components/RbCard/Card';
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg' import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
import { getMemoryEmotionConfig, updateMemoryEmotionConfig } from '@/api/memory' import { getMemoryEmotionConfig, updateMemoryEmotionConfig } from '@/api/memory'
@@ -11,6 +24,9 @@ import { getModelListUrl } from '@/api/models'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem' import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
/**
* Configuration field definitions
*/
const configList = [ const configList = [
{ {
key: 'emotion_enabled', key: 'emotion_enabled',
@@ -41,6 +57,9 @@ const configList = [
}, },
] ]
/**
* Emotion engine configuration component
*/
const EmotionEngine: React.FC = () => { const EmotionEngine: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useParams(); const { id } = useParams();
@@ -55,6 +74,7 @@ const EmotionEngine: React.FC = () => {
getConfigData() getConfigData()
}, [id]) }, [id])
/** Fetch emotion engine configuration */
const getConfigData = () => { const getConfigData = () => {
if (!id) { if (!id) {
return return
@@ -72,9 +92,11 @@ const EmotionEngine: React.FC = () => {
console.error('Failed to load data'); console.error('Failed to load data');
}) })
} }
/** Reset form to saved configuration */
const handleReset = () => { const handleReset = () => {
form.setFieldsValue(configData); form.setFieldsValue(configData);
} }
/** Save emotion engine configuration */
const handleSave = () => { const handleSave = () => {
if (!id) { if (!id) {
return return

View File

@@ -1,48 +1,99 @@
// 标签表单数据类型 /*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:57:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:57:37
*/
/**
* Type definitions for Emotion Engine
*/
/**
* Tag form data (legacy, not used in current implementation)
*/
export interface TagFormData { export interface TagFormData {
/** Tag name */
tagName: string; tagName: string;
/** Tag type */
type: string; type: string;
/** Tag color */
color: string; color: string;
/** Tag description */
description?: string; description?: string;
/** Applicable scope */
applicableScope?: string[]; applicableScope?: string[];
/** Semantic expansion */
semanticExpansion?: string; semanticExpansion?: string;
/** Whether tag is active */
isActive?: boolean; isActive?: boolean;
// 扩展字段用于区分编辑和新增操作 /** Whether in editing mode */
isEditing?: boolean; isEditing?: boolean;
/** Tag ID */
tagId?: string; tagId?: string;
} }
// 记忆总览数据类型 /**
* Memory overview record (legacy, not used in current implementation)
*/
export interface MemoryOverviewRecord { export interface MemoryOverviewRecord {
/** Record ID */
id: number; id: number;
/** Memory ID */
memoryID: string, memoryID: string,
/** Content summary */
contentSummary: string; contentSummary: string;
/** Memory type */
type: string; type: string;
/** Creation time */
createTime: string; createTime: string;
/** Last call time */
lastCallTime: string; lastCallTime: string;
/** Retention degree */
retentionDegree: string; retentionDegree: string;
/** Status */
status: string; status: string;
} }
// 定义组件暴露的方法接口
/**
* Memory overview form ref interface (legacy)
*/
export interface MemoryOverviewFormRef { export interface MemoryOverviewFormRef {
/** Open form with optional record */
handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void; handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void;
} }
// 遗忘曲线数据类型 /**
* Forgetting curve record (legacy, not used in current implementation)
*/
export interface CurveRecord { export interface CurveRecord {
/** Memory ID */
memoryID: string; memoryID: string;
/** Memory type */
type: string; type: string;
/** Current retention rate */
currentRetentionRate: string; currentRetentionRate: string;
/** Finally activated time */
finallyActivated: string; finallyActivated: string;
/** Expected forgetting time */
expectedForgettingTime: string; expectedForgettingTime: string;
/** Reinforcement count */
reinforcementCount: string; reinforcementCount: string;
} }
/**
* Emotion engine configuration form
*/
export interface ConfigForm { export interface ConfigForm {
/** Configuration ID */
config_id: number | string; config_id: number | string;
/** Whether emotion engine is enabled */
emotion_enabled: boolean; emotion_enabled: boolean;
/** Emotion analysis model ID */
emotion_model_id: string; emotion_model_id: string;
/** Whether to extract keywords */
emotion_extract_keywords: boolean; emotion_extract_keywords: boolean;
/** Minimum emotion intensity threshold (0-1) */
emotion_min_intensity: number; emotion_min_intensity: number;
/** Whether to enable subject extraction */
emotion_enable_subject: boolean; emotion_enable_subject: boolean;
} }

View File

@@ -1,9 +1,24 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:00:20
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:03:35
*/
/**
* Line Chart Component
* Visualizes forgetting curves based on different configurations
* Compares current config with quick/slow forgetting presets
*/
import { type FC, useRef, useEffect, useState, useMemo, useCallback } from 'react' import { type FC, useRef, useEffect, useState, useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'; import ReactEcharts from 'echarts-for-react';
import type { ConfigForm } from '../types' import type { ConfigForm } from '../types'
// 定义当前数据类型 /**
* Current data item type
*/
type CurrentDataItem = { type CurrentDataItem = {
name: string; name: string;
data: number[]; data: number[];
@@ -16,7 +31,9 @@ type CurrentDataItem = {
emphasis: { focus: string }; emphasis: { focus: string };
}; };
// 定义图表系列数据类型 /**
* Chart series data item type
*/
type SeriesDataItem = { type SeriesDataItem = {
name: string; name: string;
data: number[]; data: number[];
@@ -29,17 +46,26 @@ type SeriesDataItem = {
emphasis: { focus: string }; emphasis: { focus: string };
}; };
// 定义简化的配置类型,只包含图表计算需要的属性 /**
* Simplified config type for chart calculations
*/
interface ChartConfig { interface ChartConfig {
lambda_mem: number; lambda_mem: number;
lambda_time: number; lambda_time: number;
offset: number; offset: number;
} }
/**
* Component props
*/
interface LineCardProps { interface LineCardProps {
/** Forgetting engine configuration */
config: ConfigForm config: ConfigForm
} }
/**
* ECharts series configuration
*/
const SeriesConfig = { const SeriesConfig = {
type: 'line', type: 'line',
smooth: true, smooth: true,
@@ -55,10 +81,18 @@ const SeriesConfig = {
focus: 'series' focus: 'series'
}, },
} }
/**
* Chart color palette
*/
const Colors = ['#155EEF', '#4DA8FF', '#FFB048'] const Colors = ['#155EEF', '#4DA8FF', '#FFB048']
// 快速遗忘lambda_mem=0.3lambda_time=1offset=0.05 慢速遗忘lambda_mem=1lambda_time=0.3offset=0.2 /**
* Line chart component for forgetting curves
* Formula: R = offset + (1 - offset) × e^(-λ_time × t / λ_mem)
* Quick forgetting: λ_mem=0.3, λ_time=1, offset=0.05
* Slow forgetting: λ_mem=1, λ_time=0.3, offset=0.2
*/
const LineChart: FC<LineCardProps> = ({ config }) => { const LineChart: FC<LineCardProps> = ({ config }) => {
const { t } = useTranslation() const { t } = useTranslation()
const chartRef = useRef<ReactEcharts>(null); const chartRef = useRef<ReactEcharts>(null);
@@ -92,7 +126,6 @@ const LineChart: FC<LineCardProps> = ({ config }) => {
}, [t]) }, [t])
useEffect(() => { useEffect(() => {
// 语言切换时重新生成数据
if (config) { if (config) {
getCaculateData(config) getCaculateData(config)
} }
@@ -131,8 +164,7 @@ const LineChart: FC<LineCardProps> = ({ config }) => {
} }
}, [config]) }, [config])
// 快速遗忘lambda_mem=0.3lambda_time=1offset=0.05 /** Initialize preset forgetting curves */
// 慢速遗忘lambda_mem=1lambda_time=0.3offset=0.2
const getInitData = useCallback(() => { const getInitData = useCallback(() => {
const list = seriesData.map(item => ({ const list = seriesData.map(item => ({
...item, ...item,
@@ -141,6 +173,7 @@ const LineChart: FC<LineCardProps> = ({ config }) => {
setInitialData(list) setInitialData(list)
}, [seriesData]) }, [seriesData])
/** Calculate retention rate for given days and config */
const calculateSeriesData = useCallback((days: number, data: ChartConfig | ConfigForm) => { const calculateSeriesData = useCallback((days: number, data: ChartConfig | ConfigForm) => {
const offset = Number(data.offset) const offset = Number(data.offset)
const lambda_time = Number(data.lambda_time) const lambda_time = Number(data.lambda_time)
@@ -148,10 +181,12 @@ const LineChart: FC<LineCardProps> = ({ config }) => {
// R = offset + (1 - offset) × e^(-λtime × t / (λmem × S)) // R = offset + (1 - offset) × e^(-λtime × t / (λmem × S))
return +(offset + (1 - offset) * Math.exp(-lambda_time * days / lambda_mem)).toFixed(4) return +(offset + (1 - offset) * Math.exp(-lambda_time * days / lambda_mem)).toFixed(4)
}, []) }, [])
/** Format data for all x-axis points */
const formatData = useCallback((data: ChartConfig | ConfigForm) => { const formatData = useCallback((data: ChartConfig | ConfigForm) => {
return xAxisData.map(days => Number(calculateSeriesData(days, data))) return xAxisData.map(days => Number(calculateSeriesData(days, data)))
}, [calculateSeriesData]) }, [calculateSeriesData])
/** Calculate current configuration curve data */
const getCaculateData = useCallback((data: ConfigForm) => { const getCaculateData = useCallback((data: ConfigForm) => {
if (!data) { if (!data) {
return return

View File

@@ -1,7 +1,20 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:00:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:00:12
*/
/**
* Forgetting Engine Configuration Page
* Configures memory forgetting curve parameters
* Uses Ebbinghaus forgetting curve formula: R = offset + (1 - offset) × e^(-λ_time × t / λ_mem)
*/
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Row, Col, Form, Slider, Button, Space, message } from 'antd'; import { Row, Col, Form, Slider, Button, Space, message } from 'antd';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card'; import RbCard from '@/components/RbCard/Card';
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg' import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
import LineChart from './components/LineChart' import LineChart from './components/LineChart'
@@ -9,6 +22,9 @@ import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory'
import type { ConfigForm } from './types' import type { ConfigForm } from './types'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem' import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
/**
* Configuration field definitions
*/
const configList = [ const configList = [
{ {
key: 'minimumRetention', key: 'minimumRetention',
@@ -82,6 +98,9 @@ const configList = [
}, },
] ]
/**
* Forgetting engine configuration component
*/
const ForgettingEngine: React.FC = () => { const ForgettingEngine: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useParams(); const { id } = useParams();
@@ -96,6 +115,7 @@ const ForgettingEngine: React.FC = () => {
getConfigData() getConfigData()
}, []) }, [])
/** Fetch forgetting engine configuration */
const getConfigData = () => { const getConfigData = () => {
getMemoryForgetConfig(id as string) getMemoryForgetConfig(id as string)
.then((res) => { .then((res) => {
@@ -113,9 +133,11 @@ const ForgettingEngine: React.FC = () => {
console.error('Failed to load data'); console.error('Failed to load data');
}) })
} }
/** Reset form to saved configuration */
const handleReset = () => { const handleReset = () => {
form.setFieldsValue(configData || {}); form.setFieldsValue(configData || {});
} }
/** Save forgetting engine configuration */
const handleSave = () => { const handleSave = () => {
setLoading(true) setLoading(true)
updateMemoryForgetConfig({ updateMemoryForgetConfig({

View File

@@ -1,56 +1,111 @@
// 标签表单数据类型 /*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:00:08
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:00:08
*/
/**
* Type definitions for Forgetting Engine
*/
/**
* Tag form data (legacy, not used in current implementation)
*/
export interface TagFormData { export interface TagFormData {
/** Tag name */
tagName: string; tagName: string;
/** Tag type */
type: string; type: string;
/** Tag color */
color: string; color: string;
/** Tag description */
description?: string; description?: string;
/** Applicable scope */
applicableScope?: string[]; applicableScope?: string[];
/** Semantic expansion */
semanticExpansion?: string; semanticExpansion?: string;
/** Whether tag is active */
isActive?: boolean; isActive?: boolean;
// 扩展字段用于区分编辑和新增操作 /** Whether in editing mode */
isEditing?: boolean; isEditing?: boolean;
/** Tag ID */
tagId?: string; tagId?: string;
} }
// 记忆总览数据类型 /**
* Memory overview record (legacy, not used in current implementation)
*/
export interface MemoryOverviewRecord { export interface MemoryOverviewRecord {
/** Record ID */
id: number; id: number;
/** Memory ID */
memoryID: string, memoryID: string,
/** Content summary */
contentSummary: string; contentSummary: string;
/** Memory type */
type: string; type: string;
/** Creation time */
createTime: string; createTime: string;
/** Last call time */
lastCallTime: string; lastCallTime: string;
/** Retention degree */
retentionDegree: string; retentionDegree: string;
/** Status */
status: string; status: string;
} }
// 定义组件暴露的方法接口
/**
* Memory overview form ref interface (legacy)
*/
export interface MemoryOverviewFormRef { export interface MemoryOverviewFormRef {
/** Open form with optional record */
handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void; handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void;
} }
// 遗忘曲线数据类型 /**
* Forgetting curve record (legacy, not used in current implementation)
*/
export interface CurveRecord { export interface CurveRecord {
/** Memory ID */
memoryID: string; memoryID: string;
/** Memory type */
type: string; type: string;
/** Current retention rate */
currentRetentionRate: string; currentRetentionRate: string;
/** Finally activated time */
finallyActivated: string; finallyActivated: string;
/** Expected forgetting time */
expectedForgettingTime: string; expectedForgettingTime: string;
/** Reinforcement count */
reinforcementCount: string; reinforcementCount: string;
} }
/**
* Forgetting engine configuration form
*/
export interface ConfigForm { export interface ConfigForm {
/** Configuration ID */
config_id?: string; config_id?: string;
/** Time decay factor (λ_time) */
lambda_time: string | number; lambda_time: string | number;
/** Memory strength factor (λ_mem) */
lambda_mem: string | number; lambda_mem: string | number;
/** Minimum retention offset */
offset: string | number; offset: string | number;
/** Decay constant */
decay_constant: string | number; decay_constant: string | number;
/** Maximum history length */
max_history_length: string | number; max_history_length: string | number;
/** Forgetting threshold */
forgetting_threshold: string | number; forgetting_threshold: string | number;
/** Minimum days since last access */
min_days_since_access: string | number; min_days_since_access: string | number;
/** Whether to enable LLM summary */
enable_llm_summary: boolean; enable_llm_summary: boolean;
/** Maximum merge batch size */
max_merge_batch_size: string | number; max_merge_batch_size: string | number;
/** Forgetting interval in hours */
forgetting_interval_hours: string | number; forgetting_interval_hours: string | number;
/** Additional dynamic fields */
[key: string]: any; [key: string]: any;
} }

View File

@@ -1,6 +1,21 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:27:28
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:27:28
*/
/**
* Card Component
* Reusable card wrapper for dashboard sections
*/
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
/**
* Component props
*/
interface CardProps { interface CardProps {
children: ReactNode; children: ReactNode;
title: string; title: string;

View File

@@ -1,13 +1,28 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:17:05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:18:32
*/
/**
* Line Chart Card Component
* Displays time-series data with ECharts line chart
* Supports multiple series and date range selection
*/
import { type FC, useRef } from 'react' import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Select } from 'antd' import { Select } from 'antd'
import ReactEcharts from 'echarts-for-react'; import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import Card from './Card' import Card from './Card'
/**
* Component props
*/
interface LineCardProps { interface LineCardProps {
chartData: Array<Record<string, string | number>>; chartData: Array<Record<string, string | number>>;
limit: number; limit: number;
@@ -17,6 +32,7 @@ interface LineCardProps {
seriesList: string[]; seriesList: string[];
} }
/** ECharts series configuration */
const SeriesConfig = { const SeriesConfig = {
type: 'line', type: 'line',
stack: 'Total', stack: 'Total',
@@ -34,6 +50,7 @@ const SeriesConfig = {
}, },
data: [220, 302, 181, 234, 210, 290, 150] data: [220, 302, 181, 234, 210, 290, 150]
} }
/** Chart color palette */
const Colors = ['#FFB048', '#4DA8FF', '#155EEF'] const Colors = ['#FFB048', '#4DA8FF', '#155EEF']
const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, className, seriesList }) => { const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, className, seriesList }) => {
@@ -47,6 +64,7 @@ const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, classNa
{ label: t('dashboard.lastYear'), value: 365 }, { label: t('dashboard.lastYear'), value: 365 },
] ]
/** Generate series data with gradient colors */
const getSeries = () => { const getSeries = () => {
const list = seriesList.map((key, index) => { const list = seriesList.map((key, index) => {
return { return {
@@ -71,6 +89,7 @@ const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, classNa
return list return list
} }
/** Format series list for legend */
const formatSeriesList = () => { const formatSeriesList = () => {
return seriesList.map(key => ({ return seriesList.map(key => ({
...SeriesConfig, ...SeriesConfig,
@@ -85,11 +104,11 @@ const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, classNa
<Select <Select
value={limit} value={limit}
options={options} options={options}
onChange={(value) => onChange(value, type)} onChange={(value) => onChange(String(value), type)}
style={{ width: '150px' }} style={{ width: '150px' }}
/> />
} }
className={`rb:pb-[24px] ${className}`} className={`rb:pb-6 ${className}`}
> >
{chartData && chartData.length > 0 ? ( {chartData && chartData.length > 0 ? (
<ReactEcharts <ReactEcharts
@@ -157,7 +176,7 @@ const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, classNa
notMerge={true} notMerge={true}
lazyUpdate={true} lazyUpdate={true}
/> />
) : <Empty size={120} className="rb:mt-[48px] rb:mb-[81px]" />} ) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
</Card> </Card>
) )
} }

View File

@@ -1,14 +1,30 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:16:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:16:45
*/
/**
* Pie Chart Card Component
* Displays knowledge base type distribution with ECharts donut chart
*/
import { type FC, useRef, useEffect } from 'react' import { type FC, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'; import ReactEcharts from 'echarts-for-react';
import Card from './Card' import Card from './Card'
import Loading from '@/components/Empty/Loading' import Loading from '@/components/Empty/Loading'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
/**
* Component props
*/
interface PieCardProps { interface PieCardProps {
chartData: Array<Record<string, string | number>>; chartData: Array<Record<string, string | number>>;
loading: boolean; loading: boolean;
} }
/** Chart color palette */
const Colors = ['#155EEF', '#31E8FF', '#AD88FF', '#FFB048', '#4DA8FF', '#03BDFF'] const Colors = ['#155EEF', '#31E8FF', '#AD88FF', '#FFB048', '#4DA8FF', '#03BDFF']
const PieCard: FC<PieCardProps> = ({ chartData, loading }) => { const PieCard: FC<PieCardProps> = ({ chartData, loading }) => {
@@ -45,7 +61,7 @@ const PieCard: FC<PieCardProps> = ({ chartData, loading }) => {
{loading {loading
? <Loading size={249} /> ? <Loading size={249} />
: !chartData || chartData.length === 0 : !chartData || chartData.length === 0
? <Empty size={120} className="rb:mt-[48px] rb:mb-[81px]" /> ? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
: <ReactEcharts : <ReactEcharts
option={{ option={{
color: Colors, color: Colors,

View File

@@ -1,14 +1,19 @@
/* /*
* @Description: * @Author: ZhaoYing
* @Version: 0.0.1 * @Date: 2026-02-03 17:16:38
* @Author: yujiangping * @Last Modified by: ZhaoYing
* @Date: 2026-01-05 17:22:23 * @Last Modified time: 2026-02-03 17:16:38
* @LastEditors: yujiangping
* @LastEditTime: 2026-01-15 14:55:51
*/ */
/**
* Quick Operation Component
* Displays shortcut cards for common operations
* Includes navigation to application, knowledge base, memory conversation, and help center
*/
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Card from './Card'; import Card from './Card';
import applicationIcon from '@/assets/images/menu/application_active.svg'; import applicationIcon from '@/assets/images/menu/application_active.svg';
import knowledgeIcon from '@/assets/images/menu/knowledge_active.svg'; import knowledgeIcon from '@/assets/images/menu/knowledge_active.svg';
@@ -16,6 +21,7 @@ import memoryConversationIcon from '@/assets/images/menu/memoryConversation_acti
import helpCenterIcon from '@/assets/images/menu/helpCenter_active.svg' import helpCenterIcon from '@/assets/images/menu/helpCenter_active.svg'
import arrowTopRight from '@/assets/images/home/arrow_top_right.svg'; import arrowTopRight from '@/assets/images/home/arrow_top_right.svg';
/** Quick operation items configuration */
const quickOperations = [ const quickOperations = [
{ key: 'createNewApplication', url: '/application' }, { key: 'createNewApplication', url: '/application' },
{ key: 'createNewKnowledge', url: '/knowledge-base' }, { key: 'createNewKnowledge', url: '/knowledge-base' },
@@ -23,6 +29,7 @@ const quickOperations = [
{ key: 'helpCenter', url: '' }, { key: 'helpCenter', url: '' },
] ]
/** Icon mapping for quick operations */
const quickOperationIcons: {[key: string]: string | undefined} = { const quickOperationIcons: {[key: string]: string | undefined} = {
createNewApplication: applicationIcon, createNewApplication: applicationIcon,
createNewKnowledge: knowledgeIcon, createNewKnowledge: knowledgeIcon,
@@ -33,6 +40,7 @@ const QuickOperation:FC = () => {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const navigate = useNavigate(); const navigate = useNavigate();
/** Handle navigation or external link */
const handleJump = (url: string | null) => { const handleJump = (url: string | null) => {
if (url) { if (url) {
navigate(url) navigate(url)
@@ -41,7 +49,7 @@ const QuickOperation:FC = () => {
const lang = currentLang === 'zh' ? 'zh' : 'en'; const lang = currentLang === 'zh' ? 'zh' : 'en';
const helpUrl = `https://docs.redbearai.com/s/${lang}-memorybear`; const helpUrl = `https://docs.redbearai.com/s/${lang}-memorybear`;
// 创建隐藏的 a 标签来避免弹窗拦截 /** Create hidden link to avoid popup blocking */
const link = document.createElement('a'); const link = document.createElement('a');
link.href = helpUrl; link.href = helpUrl;
link.target = '_blank'; link.target = '_blank';
@@ -55,15 +63,15 @@ const QuickOperation:FC = () => {
<Card <Card
title={t('dashboard.quickOperation')} title={t('dashboard.quickOperation')}
> >
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]"> <div className="rb:grid rb:grid-cols-4 rb:gap-4">
{quickOperations.map(item => ( {quickOperations.map(item => (
<div key={item.key} className="rb:rounded-[8px] rb:p-[20px_16px] rb:border-1 rb:border-[#DFE4ED] rb:cursor-pointer rb:hover:border-[#155EEF]" onClick={() => handleJump(item.url)}> <div key={item.key} className="rb:rounded-lg rb:p-[20px_16px] rb:border rb:border-[#DFE4ED] rb:cursor-pointer rb:hover:border-[#155EEF]" onClick={() => handleJump(item.url)}>
<div className="rb:flex rb:justify-between"> <div className="rb:flex rb:justify-between">
<img className="rb:w-[32px] rb:h-[32px]" src={quickOperationIcons[item.key]} /> <img className="rb:w-8 rb:h-8" src={quickOperationIcons[item.key]} />
<img className="rb:w-[16px] rb:h-[16px]" src={arrowTopRight} /> <img className="rb:w-4 rb:h-4" src={arrowTopRight} />
</div> </div>
<div className="rb:mt-[24px] rb:text-[#212332] rb:text-[16px] rb:leading-[20px] rb:font-medium">{t(`dashboard.${item.key}`)}</div> <div className="rb:mt-6 rb:text-[#212332] rb:text-[16px] rb:leading-5 rb:font-medium">{t(`dashboard.${item.key}`)}</div>
<div className="rb:mt-[8px] rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t(`dashboard.${item.key}Desc`)}</div> <div className="rb:mt-2 rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t(`dashboard.${item.key}Desc`)}</div>
</div> </div>
))} ))}
</div> </div>

View File

@@ -1,7 +1,20 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:15:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:15:33
*/
/**
* Recent Activity Component
* Displays recent memory processing activities with statistics
* Shows chunk count, statements, entity relations, and temporal data
*/
import { type FC, useEffect, useState } from 'react' import { type FC, useEffect, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Skeleton } from 'antd'; import { Skeleton } from 'antd';
import chunkCountIcon from '@/assets/images/home/chunk_count.svg'; import chunkCountIcon from '@/assets/images/home/chunk_count.svg';
import statementsCountIcon from '@/assets/images/home/statements_count.svg'; import statementsCountIcon from '@/assets/images/home/statements_count.svg';
import tripletCountIcon from '@/assets/images/home/triplet_count.svg'; import tripletCountIcon from '@/assets/images/home/triplet_count.svg';
@@ -11,18 +24,30 @@ import Empty from '@/components/Empty';
import Card from './Card'; import Card from './Card';
import { getRecentActivityStats } from '@/api/memory'; import { getRecentActivityStats } from '@/api/memory';
/**
* API response data structure
*/
interface Data { interface Data {
latest_relative: string; latest_relative: string;
stats: RecentActivities; stats: RecentActivities;
} }
/**
* Recent activity statistics
*/
interface RecentActivities { interface RecentActivities {
"chunk_count": number; // 数据分块 /** Data chunk count */
"statements_count": number; // 语句提取 "chunk_count": number;
"triplet_entities_count": number; // 实体关系萃取-实体节点 /** Statement extraction count */
"triplet_relations_count": number; // 实体关系萃取 - 关系连接 "statements_count": number;
"temporal_count": number; // 时间萃取 /** Entity node count */
"triplet_entities_count": number;
/** Relation connection count */
"triplet_relations_count": number;
/** Temporal extraction count */
"temporal_count": number;
} }
/** Activity list configuration */
const activityList = [ const activityList = [
{ key: 'chunk_count', icon: chunkCountIcon }, { key: 'chunk_count', icon: chunkCountIcon },
{ key: 'statements_count', icon: statementsCountIcon }, { key: 'statements_count', icon: statementsCountIcon },
@@ -40,7 +65,7 @@ const RecentActivity:FC = () => {
getRecentActivityList() getRecentActivityList()
}, []) }, [])
// 最近活动统计 /** Fetch recent activity statistics */
const getRecentActivityList = () => { const getRecentActivityList = () => {
setLoading(true) setLoading(true)
getRecentActivityStats().then(res => { getRecentActivityStats().then(res => {
@@ -58,13 +83,13 @@ const RecentActivity:FC = () => {
{loading {loading
? <Skeleton /> ? <Skeleton />
: !recentActivities || Object.keys(recentActivities).length === 0 : !recentActivities || Object.keys(recentActivities).length === 0
? <Empty url={activityEmpty} subTitle={t('dashboard.activityEmpty')} size={120} className="rb:mt-[45px] rb:mb-[81px]" /> ? <Empty url={activityEmpty} subTitle={t('dashboard.activityEmpty')} size={120} className="rb:mt-11.25 rb:mb-20.25" />
: activityList.map((item, index) => ( : activityList.map((item, index) => (
<div key={item.key} className={clsx("rb:flex rb:justify-between rb:items-center rb:not-italic", { <div key={item.key} className={clsx("rb:flex rb:justify-between rb:items-center rb:not-italic", {
'rb:mt-[24px]': index !== 0 'rb:mt-6': index !== 0
})}> })}>
<div className="rb:flex rb:items-center rb:text-[#060419] rb:text-[16px] rb:font-medium"> <div className="rb:flex rb:items-center rb:text-[#060419] rb:text-[16px] rb:font-medium">
<img className="rb:w-[40px] rb:h-[40px] rb:mr-[16px]" src={item.icon} /> <img className="rb:w-10 rb:h-10 rb:mr-4" src={item.icon} />
<div> <div>
{t(`dashboard.${item.key}`)} {t(`dashboard.${item.key}`)}
<div className="rb:text-[#5B6167] rb:text-[14px] rb:font-normal"> <div className="rb:text-[#5B6167] rb:text-[14px] rb:font-normal">

View File

@@ -1,7 +1,19 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:15:04
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:15:04
*/
/**
* Tag List Component
* Displays popular memory tags with frequency counts
*/
import { type FC, useEffect, useState } from 'react' import { type FC, useEffect, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Skeleton } from 'antd'; import { Skeleton } from 'antd';
import tagEmpty from '@/assets/images/home/tagEmpty.svg' import tagEmpty from '@/assets/images/home/tagEmpty.svg'
import Empty from '@/components/Empty'; import Empty from '@/components/Empty';
import Card from './Card'; import Card from './Card';
@@ -16,7 +28,7 @@ const TagList:FC = () => {
getTagList() getTagList()
}, []) }, [])
// 热门记忆标签 /** Fetch popular memory tags */
const getTagList = () => { const getTagList = () => {
setLoading(true) setLoading(true)
getHotMemoryTags().then(res => { getHotMemoryTags().then(res => {
@@ -30,12 +42,12 @@ const TagList:FC = () => {
{loading {loading
? <Skeleton /> ? <Skeleton />
: !tagList || tagList.length === 0 : !tagList || tagList.length === 0
? <Empty url={tagEmpty} title={t('dashboard.activityEmpty')} size={120} className="rb:mt-[36px] rb:mb-[81px]" /> ? <Empty url={tagEmpty} title={t('dashboard.activityEmpty')} size={120} className="rb:mt-9 rb:mb-20.25" />
: <div className="rb:gap-[12px] rb:flex rb:flex-wrap"> : <div className="rb:gap-3 rb:flex rb:flex-wrap">
{tagList.map((item, index) => ( {tagList.map((item, index) => (
<div <div
key={item.name} key={item.name}
className={clsx("rb:pt-[6px] rb:pb-[6px] rb:pr-[23px] rb:pl-[20px] rb:border-1 rb:leading-[20px] rb:bg-white rb:rounded-[17px]", { className={clsx("rb:pt-1.5 rb:pb-1.5 rb:pr-5.75 rb:pl-5 rb:border rb:leading-5 rb:bg-white rb:rounded-[17px]", {
'rb:border-[rgba(21,94,239,0.4)] rb:text-[#155EEF]': index % 3 === 0, 'rb:border-[rgba(21,94,239,0.4)] rb:text-[#155EEF]': index % 3 === 0,
'rb:border-[rgba(255,138,76,0.4)] rb:text-[#FF5D34]': index % 3 === 1, 'rb:border-[rgba(255,138,76,0.4)] rb:text-[#FF5D34]': index % 3 === 1,
'rb:border-[rgba(54,159,33,0.4)] rb:text-[#369F21]': index % 3 === 2, 'rb:border-[rgba(54,159,33,0.4)] rb:text-[#369F21]': index % 3 === 2,

View File

@@ -1,13 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:28:07
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:28:07
*/
/**
* Top Card List Component
* Displays dashboard summary cards for key metrics
* Shows total memory capacity, applications, knowledge bases, and API calls
*/
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import totalMemoryCapacity from '@/assets/images/home/totalMemoryCapacity.svg'; import totalMemoryCapacity from '@/assets/images/home/totalMemoryCapacity.svg';
import userMemory from '@/assets/images/home/userMemory.svg'; import userMemory from '@/assets/images/home/userMemory.svg';
import knowledgeBaseCount from '@/assets/images/home/knowledgeBaseCount.svg'; import knowledgeBaseCount from '@/assets/images/home/knowledgeBaseCount.svg';
import apiCallCount from '@/assets/images/home/apiCallCount.svg'; import apiCallCount from '@/assets/images/home/apiCallCount.svg';
import styles from './index.module.css' import styles from './index.module.css'
import clsx from 'clsx';
import type { DashboardData } from '../../index' import type { DashboardData } from '../../index'
/** Card configuration with styling */
const list = [ const list = [
{ {
key: 'totalMemoryCapacity', key: 'totalMemoryCapacity',
@@ -46,10 +59,14 @@ const list = [
background: 'linear-gradient( 180deg, #F8F6F5 0%, #FAFDFF 100%)', background: 'linear-gradient( 180deg, #F8F6F5 0%, #FAFDFF 100%)',
}, },
] ]
/**
* Component props
* @param data - Dashboard statistics data
*/
const TopCardList: FC<{data?: DashboardData}> = ({ data }) => { const TopCardList: FC<{data?: DashboardData}> = ({ data }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]"> <div className="rb:grid rb:grid-cols-4 rb:gap-4">
{list.map((item) => { {list.map((item) => {
return ( return (
<div <div
@@ -65,11 +82,7 @@ const TopCardList: FC<{data?: DashboardData}> = ({ data }) => {
</div> </div>
<div className={styles.content}> <div className={styles.content}>
{data?.[item.key] || item.value || 0} {data?.[item.key as keyof DashboardData] || 0}
<div className={styles.contentRight}>
{item.trendValue && <div className={clsx(styles.trend, styles[item.trend])}>{item.trendValue}</div>}
{item.trendDesc && <div>{t(`dashboard.${item.trendDesc}`)}</div>}
</div>
</div> </div>
</div> </div>
) )

View File

@@ -1,5 +1,16 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:12:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:26:04
*/
/**
* Home Dashboard Page
* Main dashboard displaying memory statistics, charts, activities, and quick operations
*/
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Row, Col, Space } from 'antd'; import { Row, Col } from 'antd';
import TopCardList from './components/TopCardList' import TopCardList from './components/TopCardList'
import LineCard from './components/LineCard' import LineCard from './components/LineCard'
@@ -9,6 +20,9 @@ import RecentActivity from './components/RecentActivity'
import TagList from './components/TagList' import TagList from './components/TagList'
import QuickOperation from './components/QuickOperation' import QuickOperation from './components/QuickOperation'
/**
* Dashboard statistics data
*/
export interface DashboardData { export interface DashboardData {
totalMemoryCapacity?: number; totalMemoryCapacity?: number;
application?: number; application?: number;
@@ -25,12 +39,12 @@ const Home = () => {
const [memoryIncrement, setMemoryIncrement] = useState<Array<{ updated_at: string; total_num: number; }>>([]); const [memoryIncrement, setMemoryIncrement] = useState<Array<{ updated_at: string; total_num: number; }>>([]);
const [limit, setLimit] = useState(7); const [limit, setLimit] = useState(7);
// 模拟API获取数据 /** Simulate API data fetch */
useEffect(() => { useEffect(() => {
getData() getData()
getKnowledgeTypeDistribution() getKnowledgeTypeDistribution()
}, []); }, []);
// 记忆总量 / 应用数量 / 知识库数量 / API调用次数 /** Fetch memory total, app count, knowledge base count, API call count */
const getData = () => { const getData = () => {
getDashboardData().then(res => { getDashboardData().then(res => {
const response = res as { const response = res as {
@@ -49,16 +63,16 @@ const Home = () => {
} }
} }
const { storage_type = 'neo4j' } = response || {} const { storage_type = 'neo4j' } = response || {}
const responseData = response[storage_type + '_data'] || {} const responseData = storage_type === 'neo4j' ? response.neo4j_data : response.rag_data
setDashboardData({ setDashboardData({
totalMemoryCapacity: responseData.total_memory || 0, totalMemoryCapacity: responseData?.total_memory || 0,
application: responseData.total_app || 0, application: responseData?.total_app || 0,
knowledgeBaseCount: responseData.total_knowledge || 0, knowledgeBaseCount: responseData?.total_knowledge || 0,
apiCallCount: responseData.total_api_call || 0 apiCallCount: responseData?.total_api_call || 0
}) })
}) })
} }
// 知识库类型分布 / 知识库数量 /** Fetch knowledge base type distribution */
const getKnowledgeTypeDistribution = () => { const getKnowledgeTypeDistribution = () => {
setLoading({ setLoading({
...loading, ...loading,
@@ -86,7 +100,7 @@ const Home = () => {
}) })
}) })
} }
// 记忆增长趋势 /** Fetch memory growth trend data */
const getMemoryIncrementData = () => { const getMemoryIncrementData = () => {
getMemoryIncrement(limit).then(res => { getMemoryIncrement(limit).then(res => {
const response = res as { updated_at: string; total_num: number; }[] const response = res as { updated_at: string; total_num: number; }[]
@@ -106,12 +120,10 @@ const Home = () => {
} }
return ( return (
<div className="rb:pb-[24px]"> <div className="rb:pb-6">
{/* 统计卡片 */}
<TopCardList data={dashboardData} /> <TopCardList data={dashboardData} />
<Row className="rb:mt-[16px]" gutter={16}> <Row className="rb:mt-4" gutter={16}>
{/* 记忆增长趋势 */}
<Col span={12}> <Col span={12}>
<LineCard <LineCard
chartData={memoryIncrement} chartData={memoryIncrement}
@@ -121,7 +133,6 @@ const Home = () => {
seriesList={['total_num']} seriesList={['total_num']}
/> />
</Col> </Col>
{/* 知识库类型分布 */}
<Col span={12}> <Col span={12}>
<PieCard <PieCard
loading={loading.knowledgeTypeDistribution} loading={loading.knowledgeTypeDistribution}
@@ -130,20 +141,17 @@ const Home = () => {
</Col> </Col>
</Row> </Row>
<Row className="rb:mt-[16px]" gutter={16}> <Row className="rb:mt-4" gutter={16}>
<Col span={12}> <Col span={12}>
{/* 最近记忆活动 */}
<RecentActivity /> <RecentActivity />
</Col> </Col>
<Col span={12}> <Col span={12}>
{/* 热门记忆标签 */}
<TagList /> <TagList />
</Col> </Col>
</Row> </Row>
<Row className="rb:mt-[16px]" gutter={16}> <Row className="rb:mt-4" gutter={16}>
<Col span={24}> <Col span={24}>
{/* 快速操作 */}
<QuickOperation /> <QuickOperation />
</Col> </Col>
</Row> </Row>

View File

@@ -1,10 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:37:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:05:39
*/
/**
* Invite Register Page
* Handles user registration via workspace invitation link
* Validates invite token and allows new users to set up their account
*/
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Button, Input, Form, Progress, App } from 'antd'; import { Button, Input, Form, Progress, App } from 'antd';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { ExclamationCircleFilled } from '@ant-design/icons';
import { useUser, type LoginInfo } from '@/store/user';
import type { FormProps } from 'antd'; import type { FormProps } from 'antd';
import { useUser, type LoginInfo } from '@/store/user';
import { login } from '@/api/user' import { login } from '@/api/user'
import inviteBg from '@/assets/images/login/inviteBg.png' import inviteBg from '@/assets/images/login/inviteBg.png'
import checkBg from '@/assets/images/login/checkBg.png' import checkBg from '@/assets/images/login/checkBg.png'
@@ -13,13 +26,19 @@ import { validateInviteToken } from '@/api/member'
import RbAlert from '@/components/RbAlert' import RbAlert from '@/components/RbAlert'
import styles from './index.module.css' import styles from './index.module.css'
/**
* Alert extra content wrapper
*/
const Extra = ({ children }: { children: React.ReactNode }) => ( const Extra = ({ children }: { children: React.ReactNode }) => (
<div className="rb:flex rb:items-start"> <div className="rb:flex rb:items-start">
<ExclamationCircleFilled className="rb:mr-[4px] rb:mt-[3px]" /> <ExclamationCircleFilled className="rb:mr-1 rb:mt-0.75" />
{children} {children}
</div> </div>
) )
/**
* Invite registration component
*/
const InviteRegister: React.FC = () => { const InviteRegister: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -36,6 +55,7 @@ const InviteRegister: React.FC = () => {
getInitalData() getInitalData()
}, []); }, []);
/** Fetch and validate invite token */
const getInitalData = () => { const getInitalData = () => {
if (!token) { if (!token) {
message.warning(t('user.inviteLinkInvalid')) message.warning(t('user.inviteLinkInvalid'))
@@ -49,39 +69,39 @@ const InviteRegister: React.FC = () => {
}) })
} }
// 密码强度校验函数 /** Validate password strength and return score */
const validatePasswordStrength = (password: string): { strength: 'weak' | 'medium' | 'strong', error: string } => { const validatePasswordStrength = (password: string): { strength: 'weak' | 'medium' | 'strong', error: string } => {
let strength: 'weak' | 'medium' | 'strong' = 'weak'; let strength: 'weak' | 'medium' | 'strong' = 'weak';
let score = 0; let score = 0;
let error = ''; let error = '';
// 密码长度检查 // Password length check
if (password.length < 8) { if (password.length < 8) {
error = t('login.lengthDesc'); error = t('login.lengthDesc');
return { strength, error }; return { strength, error };
} }
score += 1; score += 1;
// 包含数字 // Contains number
if (/\d/.test(password)) score += 1; if (/\d/.test(password)) score += 1;
// 包含小写字母 // Contains lowercase letter
if (/[a-z]/.test(password)) score += 1; if (/[a-z]/.test(password)) score += 1;
// 包含大写字母 // Contains uppercase letter
if (/[A-Z]/.test(password)) score += 1; if (/[A-Z]/.test(password)) score += 1;
// 包含特殊字符 // Contains special character
if (/[^A-Za-z0-9]/.test(password)) score += 1; if (/[^A-Za-z0-9]/.test(password)) score += 1;
// 判断强度 // Determine strength
if (score >= 4) { if (score >= 4) {
strength = 'strong'; strength = 'strong';
} else if (score >= 3) { } else if (score >= 3) {
strength = 'medium'; strength = 'medium';
} }
// 根据强度返回提示 // Return message based on strength
if (strength === 'weak' && score >= 1) { if (strength === 'weak' && score >= 1) {
error = t('login.weakDesc'); error = t('login.weakDesc');
} else if (strength === 'medium') { } else if (strength === 'medium') {
@@ -91,7 +111,7 @@ const InviteRegister: React.FC = () => {
return { strength, error }; return { strength, error };
}; };
// 监听密码变化,更新强度 /** Update password strength indicator on change */
const handlePasswordChange = (value: string) => { const handlePasswordChange = (value: string) => {
if (!value) { if (!value) {
setPasswordStrength(null); setPasswordStrength(null);
@@ -101,19 +121,19 @@ const InviteRegister: React.FC = () => {
setPasswordStrength(strength); setPasswordStrength(strength);
}; };
// 密码一致性校验 /** Validate password confirmation matches */
const validateConfirmPassword = (_: unknown, value: string) => { const validateConfirmPassword = (_: unknown, value: string) => {
const password = values.password; const password = values.password;
if (!value) { if (!value) {
return Promise.reject(new Error('请确认密码')); return Promise.reject(new Error('Please confirm password'));
} }
if (value !== password) { if (value !== password) {
return Promise.reject(new Error('两次输入的密码不一致')); return Promise.reject(new Error('Passwords do not match'));
} }
return Promise.resolve(); return Promise.resolve();
}; };
// 处理注册提交 /** Handle registration form submission */
const handleRegister: FormProps<LoginForm>['onFinish'] = async (values) => { const handleRegister: FormProps<LoginForm>['onFinish'] = async (values) => {
setLoading(true); setLoading(true);
login({ login({
@@ -133,12 +153,12 @@ const InviteRegister: React.FC = () => {
return ( return (
<div className="rb:w-screen rb:h-screen rb:flex rb:items-center rb:justify-center"> <div className="rb:w-screen rb:h-screen rb:flex rb:items-center rb:justify-center">
<img src={inviteBg} className="rb:w-screen rb:h-screen rb:fixed rb:top-0 rb:left-0 rb:z-[0]" /> <img src={inviteBg} className="rb:w-screen rb:h-screen rb:fixed rb:top-0 rb:left-0 rb:z-0" />
<div className="rb:relative rb:z-[1] rb:w-[480px] rb:max-h-full rb:overflow-y-auto rb:bg-[#FFFFFF] rb:rounded-[12px] rb:shadow-[0px_2px_10px_0px_rgba(11,49,124,0.2)]"> <div className="rb:relative rb:z-1 rb:w-120 rb:max-h-full rb:overflow-y-auto rb:bg-[#FFFFFF] rb:rounded-xl rb:shadow-[0px_2px_10px_0px_rgba(11,49,124,0.2)]">
<div className="rb:bg-[url('@/assets/images/login/inviteForm.png')] rb:bg-cover rb:bg-no-repeat rb:text-[24px] rb:font-bold rb:leading-[32px] rb:p-[28px_24px]"> <div className="rb:bg-[url('@/assets/images/login/inviteForm.png')] rb:bg-cover rb:bg-no-repeat rb:text-[24px] rb:font-bold rb:leading-8 rb:p-[28px_24px]">
{t('login.welcomeTeam')} {t('login.welcomeTeam')}
<div className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular rb:leading-[16px] rb:mt-[10px]">{t('login.welcomeTeamSubTitle')}</div> <div className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular rb:leading-4 rb:mt-2.5">{t('login.welcomeTeamSubTitle')}</div>
</div> </div>
<Form <Form
form={form} form={form}
@@ -146,10 +166,10 @@ const InviteRegister: React.FC = () => {
layout="vertical" layout="vertical"
className={styles.form} className={styles.form}
> >
<RbAlert icon={<img src={checkBg} className="rb:w-[24px] rb:h-[24px]" />} className="rb:mb-[24px]"> <RbAlert icon={<img src={checkBg} className="rb:w-6 rb:h-6" />} className="rb:mb-6">
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px]"> <div className="rb:text-[14px] rb:font-medium rb:leading-5">
{t('login.invitationVerified')} {t('login.invitationVerified')}
<div className="rb:text-[12px] rb:font-regular rb:leading-[16px] rb:mt-[4px]">{t('login.account')}: {values?.email || '-'}</div> <div className="rb:text-[12px] rb:font-regular rb:leading-4 rb:mt-1">{t('login.account')}: {values?.email || '-'}</div>
</div> </div>
</RbAlert> </RbAlert>
<Form.Item <Form.Item
@@ -164,14 +184,14 @@ const InviteRegister: React.FC = () => {
label={t('login.setPassword')} label={t('login.setPassword')}
extra={ extra={
<div> <div>
<div className="rb:mb-[12px]"> <div className="rb:mb-3">
<Progress <Progress
percent={passwordStrength === 'weak' ? 33 : passwordStrength === 'medium' ? 66 : passwordStrength === 'strong' ? 100 : 0} percent={passwordStrength === 'weak' ? 33 : passwordStrength === 'medium' ? 66 : passwordStrength === 'strong' ? 100 : 0}
steps={3} steps={3}
showInfo={false} showInfo={false}
style={{width: '100%'}} style={{width: '100%'}}
/> />
<div className="rb:font-medium rb:mt-[8px]"> <div className="rb:font-medium rb:mt-2">
{t('login.passwordStrength')}: {t('login.passwordStrength')}:
{passwordStrength {passwordStrength
? <span className="rb:text-[#155EEF]">{t(`login.${passwordStrength}`)}</span> ? <span className="rb:text-[#155EEF]">{t(`login.${passwordStrength}`)}</span>
@@ -191,7 +211,7 @@ const InviteRegister: React.FC = () => {
} }
const { error } = validatePasswordStrength(value); const { error } = validatePasswordStrength(value);
if (error && value.length >= 8) { if (error && value.length >= 8) {
return Promise.resolve(); // 强度提示但不阻止提交 return Promise.resolve();
} else if (error) { } else if (error) {
return Promise.reject(new Error(error)); return Promise.reject(new Error(error));
} }
@@ -221,7 +241,7 @@ const InviteRegister: React.FC = () => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="username" name="username"
label={<>{t('login.name')}<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[20px]"> {t('login.nameSubTitle')}</span></>} label={<>{t('login.name')}<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5"> {t('login.nameSubTitle')}</span></>}
> >
<Input placeholder={t('login.namePlaceholder')} /> <Input placeholder={t('login.namePlaceholder')} />
</Form.Item> </Form.Item>
@@ -230,7 +250,7 @@ const InviteRegister: React.FC = () => {
block block
loading={loading} loading={loading}
htmlType="submit" htmlType="submit"
className="rb:h-[40px]! rb:rounded-[8px]!" className="rb:h-10! rb:rounded-lg!"
> >
{t('login.register')} {t('login.register')}
</Button> </Button>

View File

@@ -1,14 +1,41 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:37:20
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:37:20
*/
/**
* Type definitions for Invite Register
*/
/**
* Registration form data
*/
export interface LoginForm { export interface LoginForm {
/** User email address */
email: string; email: string;
/** User password */
password: string; password: string;
/** Password confirmation */
confirmPassword: string; confirmPassword: string;
/** User display name */
username: string; username: string;
} }
/**
* Invite token validation response
*/
export interface ValidateToken { export interface ValidateToken {
/** Workspace name */
workspace_name: string; workspace_name: string;
/** Workspace ID */
workspace_id: string; workspace_id: string;
/** Invited user email */
email: string; email: string;
/** User role in workspace */
role: string; role: string;
/** Whether token is expired */
is_expired: boolean; is_expired: boolean;
/** Whether token is valid */
is_valid: boolean; is_valid: boolean;
} }

View File

@@ -1,8 +1,21 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:40:01
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:40:32
*/
/**
* Login Page
* Handles user authentication and login
* Features split-screen design with branding and login form
*/
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Input, Form, App } from 'antd'; import { Button, Input, Form, App } from 'antd';
import { useUser, type LoginInfo } from '@/store/user';
import type { FormProps } from 'antd'; import type { FormProps } from 'antd';
import { useUser, type LoginInfo } from '@/store/user';
import { login } from '@/api/user' import { login } from '@/api/user'
import loginBg from '@/assets/images/login/loginBg.png' import loginBg from '@/assets/images/login/loginBg.png'
import check from '@/assets/images/login/check.png' import check from '@/assets/images/login/check.png'
@@ -10,8 +23,14 @@ import email from '@/assets/images/login/email.svg'
import lock from '@/assets/images/login/lock.svg' import lock from '@/assets/images/login/lock.svg'
import type { LoginForm } from './types'; import type { LoginForm } from './types';
/**
* Input field styling
*/
const inputClassName = "rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]!" const inputClassName = "rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]!"
const LoginPage: React.FC = () => {
/**
* Login page component
*/const LoginPage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { clearUserInfo, updateLoginInfo, getUserInfo } = useUser(); const { clearUserInfo, updateLoginInfo, getUserInfo } = useUser();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -22,7 +41,7 @@ const LoginPage: React.FC = () => {
clearUserInfo(); clearUserInfo();
}, []); }, []);
// 处理登录提交 /** Handle login form submission */
const handleLogin: FormProps<LoginForm>['onFinish'] = async (values) => { const handleLogin: FormProps<LoginForm>['onFinish'] = async (values) => {
if (!values.email) { if (!values.email) {
message.warning(t('login.emailPlaceholder')); message.warning(t('login.emailPlaceholder'));
@@ -48,18 +67,18 @@ const LoginPage: React.FC = () => {
<div className="rb:min-h-screen rb:flex rb:h-screen"> <div className="rb:min-h-screen rb:flex rb:h-screen">
<div className="rb:relative rb:w-1/2 rb:h-screen rb:overflow-hidden"> <div className="rb:relative rb:w-1/2 rb:h-screen rb:overflow-hidden">
<img src={loginBg} alt="loginBg" className="rb:w-full rb:h-full rb:object-cover rb:absolute rb:top-1/2 rb:-translate-y-1/2 rb:left-0" /> <img src={loginBg} alt="loginBg" className="rb:w-full rb:h-full rb:object-cover rb:absolute rb:top-1/2 rb:-translate-y-1/2 rb:left-0" />
<div className="rb:absolute rb:top-[56px] rb:left-[64px]"> <div className="rb:absolute rb:top-14 rb:left-16">
<div className="rb:text-[28px] rb:leading-[33px] rb:font-bold rb:font-[AlimamaShuHeiTi,AlimamaShuHeiTi] rb:mb-[16px]">{t('login.title')}</div> <div className="rb:text-[28px] rb:leading-8.25 rb:font-bold rb:font-[AlimamaShuHeiTi,AlimamaShuHeiTi] rb:mb-4">{t('login.title')}</div>
<div className="rb:text-[18px] rb:leading-[25px] rb:font-regular">{t('login.subTitle')}</div> <div className="rb:text-[18px] rb:leading-6.25 rb:font-regular">{t('login.subTitle')}</div>
</div> </div>
<div className="rb:absolute rb:bottom-[81px] rb:left-[64px] rb:grid rb:grid-cols-2 rb:gap-x-[120px] rb:gap-y-[43px]"> <div className="rb:absolute rb:bottom-20.25 rb:left-16 rb:grid rb:grid-cols-2 rb:gap-x-30 rb:gap-y-10.75">
{['intelligentMemory', 'instantRecall', 'knowledgeAssociation'].map(key => ( {['intelligentMemory', 'instantRecall', 'knowledgeAssociation'].map(key => (
<div key={key} className="rb:flex"> <div key={key} className="rb:flex">
<img src={check} className="rb:w-[16px] rb:h-[16px] rb:mr-[8px] rb:mt-[3px]" /> <img src={check} className="rb:w-4 rb:h-4 rb:mr-2 rb:mt-0.75" />
<div className="rb:text-[16px] rb:leading-[22px]"> <div className="rb:text-[16px] rb:leading-5.5">
<div className="rb:font-medium">{t(`login.${key}`)}</div> <div className="rb:font-medium">{t(`login.${key}`)}</div>
<div className="rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:font-regular! rb:mt-[8px]">{t(`login.${key}Desc`)}</div> <div className="rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:font-regular! rb:mt-2">{t(`login.${key}Desc`)}</div>
</div> </div>
</div> </div>
))} ))}
@@ -67,22 +86,22 @@ const LoginPage: React.FC = () => {
</div> </div>
<div className="rb:bg-[#FFFFFF] rb:flex rb:items-center rb:justify-center rb:flex-[1_1_auto]"> <div className="rb:bg-[#FFFFFF] rb:flex rb:items-center rb:justify-center rb:flex-[1_1_auto]">
<div className="rb:w-[400px] rb:mx-auto"> <div className="rb:w-100 rb:mx-auto">
<div className="rb:text-center rb:text-[28px] rb:font-semibold rb:leading-[32px] rb:mb-[48px]">{t('login.welcome')}</div> <div className="rb:text-center rb:text-[28px] rb:font-semibold rb:leading-8 rb:mb-12">{t('login.welcome')}</div>
<Form <Form
form={form} form={form}
onFinish={handleLogin} onFinish={handleLogin}
> >
<Form.Item name="email" className="rb:mb-[20px]!"> <Form.Item name="email" className="rb:mb-5!">
<Input <Input
prefix={<img src={email} className="rb:w-[20px] rb:h-[20px] rb:mr-[8px]" />} prefix={<img src={email} className="rb:w-5 rb:h-5 rb:mr-2" />}
placeholder={t('login.emailPlaceholder')} placeholder={t('login.emailPlaceholder')}
className={inputClassName} className={inputClassName}
/> />
</Form.Item> </Form.Item>
<Form.Item name="password"> <Form.Item name="password">
<Input.Password <Input.Password
prefix={<img src={lock} className="rb:w-[20px] rb:h-[20px] rb:mr-[8px]" />} prefix={<img src={lock} className="rb:w-5 rb:h-5 rb:mr-2" />}
placeholder={t('login.passwordPlaceholder')} placeholder={t('login.passwordPlaceholder')}
className={inputClassName} className={inputClassName}
/> />
@@ -92,7 +111,7 @@ const LoginPage: React.FC = () => {
block block
loading={loading} loading={loading}
htmlType="submit" htmlType="submit"
className="rb:h-[40px]! rb:rounded-[8px]! rb:mt-[16px]" className="rb:h-10! rb:rounded-lg! rb:mt-4"
> >
{t('login.loginIn')} {t('login.loginIn')}
</Button> </Button>

View File

@@ -1,4 +1,19 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:40:06
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:40:06
*/
/**
* Type definitions for Login
*/
/**
* Login form data
*/
export interface LoginForm { export interface LoginForm {
/** User email address */
email: string; email: string;
/** User password */
password: string; password: string;
} }

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:42:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:42:17
*/
/**
* Member Modal
* Modal for inviting new members or editing existing member roles
* Generates invitation links for new members
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, Select, Modal, App } from 'antd'; import { Form, Input, Select, Modal, App } from 'antd';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
@@ -12,10 +24,17 @@ const FormItem = Form.Item;
const { Option } = Select; const { Option } = Select;
type LabelRender = SelectProps['labelRender']; type LabelRender = SelectProps['labelRender'];
/**
* Component props
*/
interface MemberModalProps { interface MemberModalProps {
/** Callback to refresh member list */
refreshTable: () => void; refreshTable: () => void;
} }
/**
* Modal for member invitation and editing
*/
const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
refreshTable refreshTable
}, ref) => { }, ref) => {
@@ -36,7 +55,7 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
] ]
const values: MemberModalData = Form.useWatch([], form); const values: MemberModalData = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
setEditingUser(null); setEditingUser(null);
@@ -44,10 +63,11 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
setLoading(false) setLoading(false)
}; };
/** Open modal with optional member data for editing */
const handleOpen = (member?: Member | null) => { const handleOpen = (member?: Member | null) => {
if (member) { if (member) {
setEditingUser(member); setEditingUser(member);
// 设置表单值 // Set form values
form.setFieldsValue({ form.setFieldsValue({
email: member.account, email: member.account,
role: member.role role: member.role
@@ -57,7 +77,7 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
} }
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save member (invite or update) */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -100,11 +120,12 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
}); });
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
})); }));
/** Custom label renderer for role select */
const labelRender: LabelRender = (props) => { const labelRender: LabelRender = (props) => {
const { label, value } = props; const { label, value } = props;

View File

@@ -1,35 +1,49 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:42:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:42:12
*/
/**
* Member Management Page
* Manages workspace members with invite, edit, and delete functionality
*/
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { App, Button, Space } from 'antd'; import { App, Button, Space } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import type { AnyObject } from 'antd/es/_util/type'; import type { AnyObject } from 'antd/es/_util/type';
import { deleteMember, memberListUrl } from '@/api/member';
import { deleteMember, memberListUrl } from '@/api/member';
import MemberModal from './components/MemberModal'; import MemberModal from './components/MemberModal';
import type { Member, MemberModalRef } from './types' import type { Member, MemberModalRef } from './types'
import Tag from '@/components/Tag'; import Tag from '@/components/Tag';
import Table, { type TableRef } from '@/components/Table' import Table, { type TableRef } from '@/components/Table'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
/**
* Member management main component
*/
const MemberManagement: React.FC = () => { const MemberManagement: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message, modal } = App.useApp(); const { message, modal } = App.useApp();
const memberFormRef = useRef<MemberModalRef>(null); const memberFormRef = useRef<MemberModalRef>(null);
const tableRef = useRef<TableRef>(null); const tableRef = useRef<TableRef>(null);
// 打开新增用户弹窗 /** Open member modal for create or edit */
const handleEdit = (member?: Member) => { const handleEdit = (member?: Member) => {
if (memberFormRef.current) { if (memberFormRef.current) {
memberFormRef.current.handleOpen(member); memberFormRef.current.handleOpen(member);
} }
} }
// 刷新列表数据 /** Refresh member list */
const refreshTable = () => { const refreshTable = () => {
tableRef.current?.loadData() tableRef.current?.loadData()
} }
// 单个删除用户 /** Delete member with confirmation */
const handleDelete = async (member: Member) => { const handleDelete = async (member: Member) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: member.username }), title: t('common.confirmDeleteDesc', { name: member.username }),
@@ -46,7 +60,7 @@ const MemberManagement: React.FC = () => {
}) })
}; };
// 表格列配置 /** Table column configuration */
const columns: ColumnsType = [ const columns: ColumnsType = [
{ {
title: t('member.username'), title: t('member.username'),

View File

@@ -1,17 +1,43 @@
// 用户数据类型 /*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:42:00
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:42:00
*/
/**
* Type definitions for Member Management
*/
/**
* Member data structure
*/
export interface Member { export interface Member {
/** Member ID */
id: string; id: string;
/** Member username */
username: string; username: string;
/** Member account (email) */
account: string; account: string;
/** Member role */
role: string; role: string;
/** Last login timestamp */
last_login_at: string | number; last_login_at: string | number;
} }
// 用户表单数据类型
/**
* Member invitation/edit form data
*/
export interface MemberModalData { export interface MemberModalData {
/** Member email address */
email: string; email: string;
/** Member role */
role: string; role: string;
} }
// 定义组件暴露的方法接口
/**
* Member modal ref interface
*/
export interface MemberModalRef { export interface MemberModalRef {
/** Open modal with optional member data for editing */
handleOpen: (user?: Member | null) => void; handleOpen: (user?: Member | null) => void;
} }

View File

@@ -1,6 +1,15 @@
/**
* Card Component
* Styled wrapper for conversation and analysis panels
* Provides consistent layout and styling
*/
import { Card } from 'antd' import { Card } from 'antd'
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
/**
* Component props
*/
interface RbCardProps { interface RbCardProps {
children: ReactNode; children: ReactNode;
title: string; title: string;

View File

@@ -1,6 +1,20 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:09:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:09:03
*/
/**
* Memory Conversation Page
* Interactive conversation interface with memory analysis
* Supports deep thinking, normal reply, and quick reply modes
*/
import { type FC, type ReactNode, useState, useEffect } from 'react' import { type FC, type ReactNode, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Col, Row, App, Skeleton, Space, Select, Flex } from 'antd' import { Col, Row, App, Skeleton, Space, Select, Flex } from 'antd'
import dayjs from 'dayjs'
import type { AnyObject } from 'antd/es/_util/type';
import clsx from 'clsx' import clsx from 'clsx'
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg' import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
@@ -19,46 +33,61 @@ import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingCh
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg' import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg' import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
import type { ChatItem } from '@/components/Chat/types' import type { ChatItem } from '@/components/Chat/types'
import dayjs from 'dayjs'
import type { AnyObject } from 'antd/es/_util/type';
/** Search mode configuration */
const searchSwitchList = [ const searchSwitchList = [
{ {
icon: DeepThinkingIcon, icon: DeepThinkingIcon,
checkedIcon: DeepThinkingCheckedIcon, checkedIcon: DeepThinkingCheckedIcon,
value: '0', value: '0',
label: 'deepThinking' // 深度思考 label: 'deepThinking'
}, },
{ {
icon: MemoryFunctionIcon, icon: MemoryFunctionIcon,
checkedIcon: MemoryFunctionCheckedIcon, checkedIcon: MemoryFunctionCheckedIcon,
value: '1', value: '1',
label: 'normalReply' // 普通回复 label: 'normalReply'
}, },
{ {
icon: OnlineIcon, icon: OnlineIcon,
checkedIcon: OnlineCheckedIcon, checkedIcon: OnlineCheckedIcon,
value: '2', value: '2',
label: 'quickReply' // 快速回复 label: 'quickReply'
}, },
] ]
/**
* Test parameters for conversation API
*/
export interface TestParams { export interface TestParams {
/** End user identifier */
end_user_id: string; end_user_id: string;
/** User message content */
message: string; message: string;
/** Search mode switch (0: deep thinking, 1: normal, 2: quick) */
search_switch: string; search_switch: string;
/** Conversation history */
history: { role: string; content: string }[]; history: { role: string; content: string }[];
/** Enable web search */
web_search?: boolean; web_search?: boolean;
/** Enable memory function */
memory?: boolean; memory?: boolean;
/** Conversation ID */
conversation_id?: string; conversation_id?: string;
} }
/**
* Data item in analysis logs
*/
interface DataItem { interface DataItem {
id: string; id: string;
question: string; question: string;
type: string; type: string;
reason: string; reason: string;
} }
/**
* Log item for conversation analysis
*/
export interface LogItem { export interface LogItem {
type: string; type: string;
title: string; title: string;
@@ -72,6 +101,9 @@ export interface LogItem {
index?: number; index?: number;
} }
/**
* Content wrapper component for analysis items
*/
const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => ( const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
<div className="rb:p-3 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg"> <div className="rb:p-3 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
{children} {children}
@@ -89,6 +121,7 @@ const MemoryConversation: FC = () => {
const [search_switch, setSearchSwitch] = useState('0') const [search_switch, setSearchSwitch] = useState('0')
const [msg, setMsg] = useState<string>('') const [msg, setMsg] = useState<string>('')
/** Load user list on mount */
useEffect(() => { useEffect(() => {
getUserMemoryList().then(res => { getUserMemoryList().then(res => {
setUserList((res as Data[] || []).map(item => ({ setUserList((res as Data[] || []).map(item => ({
@@ -98,6 +131,7 @@ const MemoryConversation: FC = () => {
}) })
}, []) }, [])
/** Handle message send */
const handleSend = () => { const handleSend = () => {
if(!userId) { if(!userId) {
message.warning(t('common.inputPlaceholder', { title: t('memoryConversation.userID') })) message.warning(t('common.inputPlaceholder', { title: t('memoryConversation.userID') }))
@@ -121,6 +155,7 @@ const MemoryConversation: FC = () => {
}) })
} }
/** Handle search mode change */
const handleChange = (value: string) => { const handleChange = (value: string) => {
setSearchSwitch(value) setSearchSwitch(value)
} }

View File

@@ -1,9 +1,24 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:30:51
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:30:51
*/
/**
* Card Component
* Collapsible card wrapper for configuration sections
*/
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import clsx from 'clsx'; import clsx from 'clsx';
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import down from '@/assets/images/userMemory/down.svg' import down from '@/assets/images/userMemory/down.svg'
/**
* Component props
*/
interface CardProps { interface CardProps {
type?: string; type?: string;
title: string | ReactNode; title: string | ReactNode;
@@ -35,7 +50,7 @@ const Card: FC<CardProps> = ({
headerType="borderless" headerType="borderless"
extra={type && handleExpand && ( extra={type && handleExpand && (
<div <div
className="rb:flex rb:items-center rb:text-[14px] rb:text-[#5B6167] rb:cursor-pointer rb:font-regular rb:leading-[20px]" className="rb:flex rb:items-center rb:text-[14px] rb:text-[#5B6167] rb:cursor-pointer rb:font-regular rb:leading-5"
onClick={() => handleExpand(type)} onClick={() => handleExpand(type)}
> >
{expanded ? t('common.foldUp') : t('common.expanded')} {expanded ? t('common.foldUp') : t('common.expanded')}

View File

@@ -1,9 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:30:11
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:08:49
*/
/**
* Result Component
* Displays real-time extraction results with progress tracking
* Shows text preprocessing, knowledge extraction, node/edge creation, and deduplication
*/
import { type FC, useState } from 'react' import { type FC, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Space, Button, Progress } from 'antd' import { Space, Button, Progress } from 'antd'
import { ExclamationCircleFilled, CheckCircleFilled, ClockCircleOutlined, LoadingOutlined } from '@ant-design/icons' import { ExclamationCircleFilled, CheckCircleFilled, ClockCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import clsx from 'clsx' import clsx from 'clsx'
import type { AnyObject } from 'antd/es/_util/type';
import Card from './Card' import Card from './Card'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import RbAlert from '@/components/RbAlert' import RbAlert from '@/components/RbAlert'
@@ -13,18 +27,24 @@ import { type SSEMessage } from '@/utils/stream'
import Tag, { type TagProps } from '@/components/Tag' import Tag, { type TagProps } from '@/components/Tag'
import Markdown from '@/components/Markdown' import Markdown from '@/components/Markdown'
import { groupDataByType } from '../constant' import { groupDataByType } from '../constant'
import type { AnyObject } from 'antd/es/_util/type';
/** Result metric mapping */
const resultObj = { const resultObj = {
extractTheNumberOfEntities: 'entities.extracted_count', extractTheNumberOfEntities: 'entities.extracted_count',
numberOfEntityDisambiguation: 'disambiguation.block_count', numberOfEntityDisambiguation: 'disambiguation.block_count',
memoryFragments: 'memory.chunks', memoryFragments: 'memory.chunks',
numberOfRelationalTriples: 'triplets.count' numberOfRelationalTriples: 'triplets.count'
} }
/**
* Component props
*/
interface ResultProps { interface ResultProps {
loading: boolean; loading: boolean;
handleSave: () => void; handleSave: () => void;
} }
/**
* Module processing item
*/
interface ModuleItem { interface ModuleItem {
status: 'pending' | 'processing' | 'completed' | 'failed'; status: 'pending' | 'processing' | 'completed' | 'failed';
data: any[], data: any[],
@@ -32,6 +52,7 @@ interface ModuleItem {
start_at?: number; start_at?: number;
end_at?: number; end_at?: number;
} }
/** Tag color mapping by status */
const tagColors: { const tagColors: {
[key: string]: TagProps['color'] [key: string]: TagProps['color']
} = { } = {
@@ -40,6 +61,7 @@ const tagColors: {
completed: 'success', completed: 'success',
failed: 'error' failed: 'error'
} }
/** Initial module state */
const initObj = { const initObj = {
data: [], data: [],
status: 'pending', status: 'pending',
@@ -57,6 +79,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
const [creatingNodesEdges, setCreatingNodesEdges] = useState<ModuleItem>(initObj as ModuleItem) const [creatingNodesEdges, setCreatingNodesEdges] = useState<ModuleItem>(initObj as ModuleItem)
const [deduplication, setDeduplication] = useState<ModuleItem>(initObj as ModuleItem) const [deduplication, setDeduplication] = useState<ModuleItem>(initObj as ModuleItem)
/** Run pilot test */
const handleRun = () => { const handleRun = () => {
if(!id) return if(!id) return
setTextPreprocessing({...initObj} as ModuleItem) setTextPreprocessing({...initObj} as ModuleItem)
@@ -68,20 +91,20 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
list.forEach((data: AnyObject) => { list.forEach((data: AnyObject) => {
switch(data.event) { switch(data.event) {
case 'text_preprocessing': // 开始预处理文本 case 'text_preprocessing': // Start text preprocessing
setTextPreprocessing(prev => ({ setTextPreprocessing(prev => ({
...prev, ...prev,
status: 'processing', status: 'processing',
start_at: data.data.time start_at: data.data.time
})) }))
break break
case 'text_preprocessing_result': // 预处理文本分块中 case 'text_preprocessing_result': // Text preprocessing in progress
setTextPreprocessing(prev => ({ setTextPreprocessing(prev => ({
...prev, ...prev,
data: [...prev.data, data.data?.data] data: [...prev.data, data.data?.data]
})) }))
break break
case 'text_preprocessing_complete': // 预处理文本完成 case 'text_preprocessing_complete': // Text preprocessing complete
setTextPreprocessing(prev => ({ setTextPreprocessing(prev => ({
...prev, ...prev,
result: data.data?.data, result: data.data?.data,
@@ -89,20 +112,20 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
end_at: data.data.time end_at: data.data.time
})) }))
break break
case 'knowledge_extraction': // 开始知识抽取 case 'knowledge_extraction': // Start knowledge extraction
setKnowledgeExtraction(prev => ({ setKnowledgeExtraction(prev => ({
...prev, ...prev,
status: 'processing', status: 'processing',
start_at: data.data.time start_at: data.data.time
})) }))
break break
case 'knowledge_extraction_result': // 知识抽取中 case 'knowledge_extraction_result': // Knowledge extraction in progress
setKnowledgeExtraction(prev => ({ setKnowledgeExtraction(prev => ({
...prev, ...prev,
data: [...prev.data, data.data?.data] data: [...prev.data, data.data?.data]
})) }))
break break
case 'knowledge_extraction_complete': // 知识抽取完成 case 'knowledge_extraction_complete': // Knowledge extraction complete
setKnowledgeExtraction(prev => ({ setKnowledgeExtraction(prev => ({
...prev, ...prev,
result: data.data?.data, result: data.data?.data,
@@ -110,20 +133,20 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
end_at: data.data.time end_at: data.data.time
})) }))
break break
case 'creating_nodes_edges': // 开始创建节点和边 case 'creating_nodes_edges': // Start creating nodes and edges
setCreatingNodesEdges(prev => ({ setCreatingNodesEdges(prev => ({
...prev, ...prev,
status: 'processing', status: 'processing',
start_at: data.data.time start_at: data.data.time
})) }))
break break
case 'creating_nodes_edges_result': // 创建节点和边中 case 'creating_nodes_edges_result': // Creating nodes and edges in progress
setCreatingNodesEdges(prev => ({ setCreatingNodesEdges(prev => ({
...prev, ...prev,
data: [...prev.data, data.data?.data] data: [...prev.data, data.data?.data]
})) }))
break break
case 'creating_nodes_edges_complete': // 创建节点和边完成 case 'creating_nodes_edges_complete': // Creating nodes and edges complete
setCreatingNodesEdges(prev => ({ setCreatingNodesEdges(prev => ({
...prev, ...prev,
result: data.data?.data, result: data.data?.data,
@@ -131,20 +154,20 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
end_at: data.data.time end_at: data.data.time
})) }))
break break
case 'deduplication': // 开始去重消歧 case 'deduplication': // Start deduplication and disambiguation
setDeduplication(prev => ({ setDeduplication(prev => ({
...prev, ...prev,
status: 'processing', status: 'processing',
start_at: data.data.time start_at: data.data.time
})) }))
break break
case 'dedup_disambiguation_result': // 去重消歧中 case 'dedup_disambiguation_result': // Deduplication and disambiguation in progress
setDeduplication(prev => ({ setDeduplication(prev => ({
...prev, ...prev,
data: [...prev.data, data.data.data] data: [...prev.data, data.data.data]
})) }))
break break
case 'dedup_disambiguation_complete': // 去重消歧完成 case 'dedup_disambiguation_complete': // Deduplication and disambiguation complete
setDeduplication(prev => ({ setDeduplication(prev => ({
...prev, ...prev,
result: data.data?.data, result: data.data?.data,
@@ -152,9 +175,9 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
end_at: data.data.time end_at: data.data.time
})) }))
break break
case 'generating_results': // 开始生成结果 case 'generating_results': // Generating results
break break
case 'result': // 结果 case 'result': // Result
setTestResult(data.data?.extracted_result) setTestResult(data.data?.extracted_result)
break break
} }
@@ -172,6 +195,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
const completedNum = [textPreprocessing, knowledgeExtraction, creatingNodesEdges, deduplication].filter(item => item.status === 'completed').length const completedNum = [textPreprocessing, knowledgeExtraction, creatingNodesEdges, deduplication].filter(item => item.status === 'completed').length
const deduplicationData = groupDataByType(deduplication.data, 'result_type') const deduplicationData = groupDataByType(deduplication.data, 'result_type')
/** Format status tag */
const formatTag = (status: string) => { const formatTag = (status: string) => {
return ( return (
<Tag color={tagColors[status]}> <Tag color={tagColors[status]}>
@@ -181,12 +205,14 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
</Tag> </Tag>
) )
} }
/** Format processing time */
const formatTime = (data: ModuleItem, color?: string) => { const formatTime = (data: ModuleItem, color?: string) => {
if (typeof data.end_at === 'number' && typeof data.start_at === 'number') { if (typeof data.end_at === 'number' && typeof data.start_at === 'number') {
return <div className={`rb:mt-3 rb:text-[${color ?? '#155EEF'}]`}>{t('memoryExtractionEngine.time')}{data.end_at - data.start_at}ms</div> return <div className={`rb:mt-3 rb:text-[${color ?? '#155EEF'}]`}>{t('memoryExtractionEngine.time')}{data.end_at - data.start_at}ms</div>
} }
return null return null
} }
/** Convert first character to lowercase */
const lowercaseFirst = (str: string) => str.charAt(0).toLowerCase() + str.slice(1) const lowercaseFirst = (str: string) => str.charAt(0).toLowerCase() + str.slice(1)
return ( return (
<Card <Card
@@ -202,7 +228,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
<RbAlert color="blue" icon={<ExclamationCircleFilled />} className="rb:mb-3.5"> <RbAlert color="blue" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
{t('memoryExtractionEngine.processing')} {t('memoryExtractionEngine.processing')}
</RbAlert> </RbAlert>
{/* 整体进度 */} {/* Overall Progress */}
<div className="rb:mb-2"> <div className="rb:mb-2">
<div className="rb:flex rb:items-center rb:justify-between rb:text-[12px] rb:leading-4 rb:font-regular"> <div className="rb:flex rb:items-center rb:justify-between rb:text-[12px] rb:leading-4 rb:font-regular">
{t('memoryExtractionEngine.overallProgress')} {t('memoryExtractionEngine.overallProgress')}
@@ -220,7 +246,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
</RbAlert> </RbAlert>
} }
<Space size={16} direction="vertical" style={{ width: '100%' }}> <Space size={16} direction="vertical" style={{ width: '100%' }}>
{/* 文本预处理 */} {/* Text Preprocessing */}
<RbCard <RbCard
title={t(`memoryExtractionEngine.text_preprocessing`)} title={t(`memoryExtractionEngine.text_preprocessing`)}
extra={formatTag(textPreprocessing.status)} extra={formatTag(textPreprocessing.status)}
@@ -240,7 +266,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
</RbAlert> </RbAlert>
} }
</RbCard> </RbCard>
{/* 知识抽取 */} {/* Knowledge Extraction */}
<RbCard <RbCard
title={t(`memoryExtractionEngine.knowledge_extraction`)} title={t(`memoryExtractionEngine.knowledge_extraction`)}
extra={formatTag(knowledgeExtraction.status)} extra={formatTag(knowledgeExtraction.status)}
@@ -260,7 +286,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
})} })}
</RbAlert>} </RbAlert>}
</RbCard> </RbCard>
{/* 创建实体关系 */} {/* Creating Entity Relationships */}
<RbCard <RbCard
title={t(`memoryExtractionEngine.creating_nodes_edges`)} title={t(`memoryExtractionEngine.creating_nodes_edges`)}
extra={formatTag(creatingNodesEdges.status)} extra={formatTag(creatingNodesEdges.status)}
@@ -280,7 +306,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
{t('memoryExtractionEngine.creating_nodes_edges_desc', {num: creatingNodesEdges.result.entity_entity_edges_count})} {t('memoryExtractionEngine.creating_nodes_edges_desc', {num: creatingNodesEdges.result.entity_entity_edges_count})}
</RbAlert>} </RbAlert>}
</RbCard> </RbCard>
{/* 去重消歧 */} {/* Deduplication and Disambiguation */}
<RbCard <RbCard
title={t(`memoryExtractionEngine.deduplication`)} title={t(`memoryExtractionEngine.deduplication`)}
extra={formatTag(deduplication.status)} extra={formatTag(deduplication.status)}
@@ -307,7 +333,7 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
const keys = (resultObj as Record<string, string>)[key].split('.') const keys = (resultObj as Record<string, string>)[key].split('.')
return ( return (
<div key={index}> <div key={index}>
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{(testResult?.[keys[0] as keyof TestResult] as any)?.[keys[1]]}</div> <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: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"> <div className="rb:mt-1 rb:text-[12px] rb:text-[#369F21] rb:leading-3.5 rb:font-regular">
{} {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:30:02
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:30:02
*/
/**
* Memory Extraction Engine Configuration Page
* Configures entity deduplication, disambiguation, semantic anchoring, and pruning
* Supports real-time testing with example data
*/
import { type FC, useState, useEffect } from 'react' import { type FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
@@ -14,12 +26,15 @@ import Result from './components/Result'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem' import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import CustomSelect from '@/components/CustomSelect' import CustomSelect from '@/components/CustomSelect'
/** Available configuration section keys */
const keys = [ const keys = [
// 'example',
'storageLayerModule', 'storageLayerModule',
'arrangementLayerModule' 'arrangementLayerModule'
] ]
/**
* Configuration description component
*/
const ConfigDesc: FC<{ config: Variable, className?: string }> = ({config, className}) => { const ConfigDesc: FC<{ config: Variable, className?: string }> = ({config, className}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -54,6 +69,7 @@ const MemoryExtractionEngine: FC = () => {
} }
}, [values]) }, [values])
/** Fetch configuration data */
const getConfig = () => { const getConfig = () => {
if (!id) { if (!id) {
return return
@@ -79,11 +95,13 @@ const MemoryExtractionEngine: FC = () => {
} }
}, [id]) }, [id])
/** Toggle section expansion */
const handleExpand = (key: string) => { const handleExpand = (key: string) => {
const newKeys = expandedKeys.includes(key) ? expandedKeys.filter(item => item !== key) : [...expandedKeys, key] const newKeys = expandedKeys.includes(key) ? expandedKeys.filter(item => item !== key) : [...expandedKeys, key]
setExpandedKeys(newKeys) setExpandedKeys(newKeys)
} }
/** Save configuration */
const handleSave = () => { const handleSave = () => {
if (!id) { if (!id) {
return return

View File

@@ -1,3 +1,12 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:29:55
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:29:55
*/
/**
* Memory Extraction Engine Configuration Form Types
*/
export interface ConfigForm { export interface ConfigForm {
llm_id: string; llm_id: string;
config_id?: number | string; config_id?: number | string;
@@ -19,6 +28,9 @@ export interface ConfigForm {
baseline: string; baseline: string;
} }
/**
* Configuration variable definition
*/
export interface Variable { export interface Variable {
label: string; label: string;
variableName: string; variableName: string;
@@ -33,6 +45,9 @@ export interface Variable {
max?: number; max?: number;
step?: number; step?: number;
} }
/**
* Configuration section structure
*/
export interface ConfigVo { export interface ConfigVo {
type: string; type: string;
data: { data: {
@@ -41,6 +56,9 @@ export interface ConfigVo {
}[] }[]
} }
/**
* Test result data structure
*/
export interface TestResult { export interface TestResult {
generated_at: string; generated_at: string;
entities: Record<string, number>; entities: Record<string, number>;

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:33:22
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:33:22
*/
/**
* Memory Form Component
* Modal form for creating and editing memory configurations
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd'; import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -10,6 +21,9 @@ import CustomSelect from '@/components/CustomSelect';
const FormItem = Form.Item; const FormItem = Form.Item;
/**
* Component props
*/
interface MemoryFormProps { interface MemoryFormProps {
refresh: () => void; refresh: () => void;
} }
@@ -26,7 +40,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset form */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
setEditingMemory(null); setEditingMemory(null);
@@ -34,10 +48,11 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
setLoading(false); setLoading(false);
}; };
/** Open modal with optional data */
const handleOpen = (memory?: Memory | null) => { const handleOpen = (memory?: Memory | null) => {
if (memory) { if (memory) {
setEditingMemory(memory); setEditingMemory(memory);
// 设置表单值 /** Set form values */
form.setFieldsValue({ form.setFieldsValue({
config_name: memory.config_name, config_name: memory.config_name,
config_desc: memory.config_desc, config_desc: memory.config_desc,
@@ -48,7 +63,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
} }
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save configuration */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -73,7 +88,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
}); });
} }
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,15 +1,27 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:33:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:33:15
*/
/**
* Memory Management Page
* Manages memory configurations with extraction, forgetting, emotion, and reflection engines
* Displays configuration cards with navigation to engine settings
*/
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { List, Button, Space, App, Tooltip } from 'antd'; import { List, Button, Space, App, Tooltip } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import clsx from 'clsx'
import MemoryForm from './components/MemoryForm'; import MemoryForm from './components/MemoryForm';
import type { Memory, MemoryFormRef } from '@/views/MemoryManagement/types' import type { Memory, MemoryFormRef } from '@/views/MemoryManagement/types'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
// import StatusTag from '@/components/StatusTag'
import { getMemoryConfigList, deleteMemoryConfig } from '@/api/memory' import { getMemoryConfigList, deleteMemoryConfig } from '@/api/memory'
import BodyWrapper from '@/components/Empty/BodyWrapper' import BodyWrapper from '@/components/Empty/BodyWrapper'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import clsx from 'clsx'
import RbAlert from '@/components/RbAlert' import RbAlert from '@/components/RbAlert'
const MemoryManagement: React.FC = () => { const MemoryManagement: React.FC = () => {
@@ -25,6 +37,7 @@ const MemoryManagement: React.FC = () => {
loadMoreData() loadMoreData()
}, []); }, []);
/** Load configuration list */
const loadMoreData = () => { const loadMoreData = () => {
setLoading(true); setLoading(true);
getMemoryConfigList() getMemoryConfigList()
@@ -41,10 +54,11 @@ const MemoryManagement: React.FC = () => {
}); });
}; };
// 打开新增标签弹窗 /** Open create/edit modal */
const handleEdit = (config?: Memory) => { const handleEdit = (config?: Memory) => {
memoryFormRef.current?.handleOpen(config); memoryFormRef.current?.handleOpen(config);
} }
/** Delete configuration */
const handleDelete = (item: Memory) => { const handleDelete = (item: Memory) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.config_name }), title: t('common.confirmDeleteDesc', { name: item.config_name }),
@@ -61,6 +75,7 @@ const MemoryManagement: React.FC = () => {
}) })
}; };
/** Navigate to engine configuration page */
const handleClick = (id: number, type: string) => { const handleClick = (id: number, type: string) => {
switch (type) { switch (type) {
case 'memoryExtractionEngine': case 'memoryExtractionEngine':

View File

@@ -1,4 +1,12 @@
// 内存管理表单数据类型 /*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:33:01
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:33:24
*/
/**
* Memory management form data type
*/
export interface MemoryFormData { export interface MemoryFormData {
config_id?: number; config_id?: number;
config_name: string; config_name: string;
@@ -6,7 +14,9 @@ export interface MemoryFormData {
scene_id?: string; scene_id?: string;
} }
// 内存数据类型 /**
* Memory configuration data type
*/
export interface Memory { export interface Memory {
config_id: number; config_id: number;
config_name: string; config_name: string;
@@ -34,7 +44,9 @@ export interface Memory {
scene_name: string; scene_name: string;
[key: string]: string | number | boolean; [key: string]: string | number | boolean;
} }
// 定义组件暴露的方法接口 /**
* Component exposed methods interface
*/
export interface MemoryFormRef { export interface MemoryFormRef {
handleOpen: (memory?: Memory | null) => void; handleOpen: (memory?: Memory | null) => void;
} }

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:00
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:00
*/
/**
* Group Model View
* Displays composite/group models in card grid layout
* Supports filtering and configuration
*/
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx' import clsx from 'clsx'
import { Button } from 'antd' import { Button } from 'antd'
@@ -9,12 +21,16 @@ import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty'; import PageEmpty from '@/components/Empty/PageEmpty';
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
/**
* Group model list component
*/
const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => { const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [list, setList] = useState<ModelListItem[]>([]) const [list, setList] = useState<ModelListItem[]>([])
useEffect(() => { useEffect(() => {
getList() getList()
}, [query]) }, [query])
/** Fetch group model list */
const getList = () => { const getList = () => {
getModelNewList({ getModelNewList({
...query, ...query,
@@ -26,6 +42,7 @@ const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem
setList(response[0]?.models || []) setList(response[0]?.models || [])
}) })
} }
/** Format model data for display */
const formatData = (data: ModelListItem) => { const formatData = (data: ModelListItem) => {
return [ return [
{ {
@@ -46,6 +63,7 @@ const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem
] ]
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getList, getList,
})); }));

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:10
*/
/**
* Model List View
* Displays models grouped by provider with key configuration
* Shows model tags and allows viewing model details
*/
import { useRef, useState, useEffect, type FC } from 'react'; import { useRef, useState, useEffect, type FC } from 'react';
import { Button, Flex, Row, Col } from 'antd' import { Button, Flex, Row, Col } from 'antd'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -11,6 +23,9 @@ import KeyConfigModal from './components/KeyConfigModal'
import ModelListDetail from './components/ModelListDetail' import ModelListDetail from './components/ModelListDetail'
import { getLogoUrl } from './utils' import { getLogoUrl } from './utils'
/**
* Model list component
*/
const ModelList: FC<{ query: any }> = ({ query }) => { const ModelList: FC<{ query: any }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const keyConfigModalRef = useRef<KeyConfigModalRef>(null) const keyConfigModalRef = useRef<KeyConfigModalRef>(null)
@@ -19,6 +34,7 @@ const ModelList: FC<{ query: any }> = ({ query }) => {
useEffect(() => { useEffect(() => {
getList() getList()
}, [query]) }, [query])
/** Fetch model list grouped by provider */
const getList = () => { const getList = () => {
getModelNewList({ getModelNewList({
...query, ...query,
@@ -29,9 +45,11 @@ const ModelList: FC<{ query: any }> = ({ query }) => {
}) })
} }
/** Open model detail drawer */
const handleShowModel = (vo: ProviderModelItem) => { const handleShowModel = (vo: ProviderModelItem) => {
modelListDetailRef.current?.handleOpen(vo) modelListDetailRef.current?.handleOpen(vo)
} }
/** Open key configuration modal */
const handleKeyConfig = (vo: ProviderModelItem) => { const handleKeyConfig = (vo: ProviderModelItem) => {
keyConfigModalRef.current?.handleOpen(vo) keyConfigModalRef.current?.handleOpen(vo)
} }

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:14
*/
/**
* Model Square View
* Displays public model marketplace grouped by provider
* Allows adding models and viewing details
*/
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Button, Space, App, Divider, Flex, Tooltip } from 'antd' import { Button, Space, App, Divider, Flex, Tooltip } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons'; import { UsergroupAddOutlined } from '@ant-design/icons';
@@ -11,6 +23,9 @@ import Tag from '@/components/Tag';
import ModelSquareDetail from './components/ModelSquareDetail' import ModelSquareDetail from './components/ModelSquareDetail'
import { getLogoUrl } from './utils' import { getLogoUrl } from './utils'
/**
* Model square component
*/
const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => { const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp() const { message } = App.useApp()
@@ -19,6 +34,7 @@ const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPl
useEffect(() => { useEffect(() => {
getList() getList()
}, [query]) }, [query])
/** Fetch model plaza list */
const getList = () => { const getList = () => {
getModelPlaza(query) getModelPlaza(query)
.then(res => { .then(res => {
@@ -26,9 +42,11 @@ const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPl
}) })
} }
/** Open model detail drawer */
const handleMore = (vo: ModelPlaza) => { const handleMore = (vo: ModelPlaza) => {
modelSquareDetailRef.current?.handleOpen(vo) modelSquareDetailRef.current?.handleOpen(vo)
} }
/** Add model to workspace */
const handleAdd = (item: ModelPlazaItem) => { const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id) addModelPlaza(item.id)
.then(() => { .then(() => {
@@ -37,6 +55,7 @@ const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPl
}) })
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getList, getList,
})); }));

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:28
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:28
*/
/**
* Custom Model Modal
* Modal for creating and editing custom models in the model square
* Supports logo upload, type/provider selection, and tagging
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Select } from 'antd'; import { Form, Input, App, Select } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,6 +21,9 @@ import UploadImages from '@/components/Upload/UploadImages'
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models' import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
import { getFileLink } from '@/api/fileStorage' import { getFileLink } from '@/api/fileStorage'
/**
* Custom model modal component
*/
const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({
refresh refresh
}, ref) => { }, ref) => {
@@ -21,6 +36,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const formValues = Form.useWatch([], form) const formValues = Form.useWatch([], form)
/** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
setModel({} as ModelPlazaItem); setModel({} as ModelPlazaItem);
form.resetFields(); form.resetFields();
@@ -28,6 +44,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
setVisible(false); setVisible(false);
}; };
/** Open modal with optional model data for editing */
const handleOpen = (model?: ModelPlazaItem) => { const handleOpen = (model?: ModelPlazaItem) => {
if (model) { if (model) {
setIsEdit(true); setIsEdit(true);
@@ -42,6 +59,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
} }
setVisible(true); setVisible(true);
}; };
/** Update or create custom model */
const handleUpdate = (data: CustomModelForm) => { const handleUpdate = (data: CustomModelForm) => {
setLoading(true) setLoading(true)
const { type, provider, ...rest} = data const { type, provider, ...rest} = data
@@ -56,6 +74,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
setLoading(false) setLoading(false)
}); });
} }
/** Validate and save custom model */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -87,6 +106,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
}); });
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:33
*/
/**
* Group Model Modal
* Modal for creating and editing composite/group models
* Supports multiple API key configuration and load balancing
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Select } from 'antd'; import { Form, Input, App, Select } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -10,6 +22,9 @@ import UploadImages from '@/components/Upload/UploadImages'
import ModelImplement from './ModelImplement' import ModelImplement from './ModelImplement'
import { getFileLink } from '@/api/fileStorage' import { getFileLink } from '@/api/fileStorage'
/**
* Group model modal component
*/
const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
refresh refresh
}, ref) => { }, ref) => {
@@ -22,6 +37,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const type = Form.useWatch(['type'], form) const type = Form.useWatch(['type'], form)
/** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
setModel({} as ModelListItem); setModel({} as ModelListItem);
form.resetFields(); form.resetFields();
@@ -29,6 +45,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
setVisible(false); setVisible(false);
}; };
/** Open modal with optional model data for editing */
const handleOpen = (model?: ModelListItem) => { const handleOpen = (model?: ModelListItem) => {
if (model) { if (model) {
setIsEdit(true); setIsEdit(true);
@@ -44,6 +61,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
} }
setVisible(true); setVisible(true);
}; };
/** Validate and save group model */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -73,6 +91,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
}); });
} }
/** Update or create group model */
const handleUpdate = (data: CompositeModelForm) => { const handleUpdate = (data: CompositeModelForm) => {
setLoading(true) setLoading(true)
const { type, ...rest } = data const { type, ...rest } = data
@@ -90,6 +109,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
}); });
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,10 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:40
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:40
*/
/**
* Key Configuration Modal
* Modal for configuring API keys for model providers
* Allows setting API key and base URL
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd'; import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types'; import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types';
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import { updateProviderApiKeys } from '@/api/models' import { updateProviderApiKeys } from '@/api/models'
/**
* Key configuration modal component
*/
const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({ const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
refresh refresh
}, ref) => { }, ref) => {
@@ -15,6 +31,7 @@ const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
const [form] = Form.useForm<KeyConfigModalForm>(); const [form] = Form.useForm<KeyConfigModalForm>();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
/** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
setModel({} as ProviderModelItem); setModel({} as ProviderModelItem);
form.resetFields(); form.resetFields();
@@ -22,10 +39,12 @@ const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
setVisible(false); setVisible(false);
}; };
/** Open modal with provider model data */
const handleOpen = (vo: ProviderModelItem) => { const handleOpen = (vo: ProviderModelItem) => {
setVisible(true); setVisible(true);
setModel(vo); setModel(vo);
}; };
/** Save API key configuration */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -51,6 +70,7 @@ const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
}); });
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:20
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:54:54
*/
/**
* Sub-Model Modal
* Modal for selecting models and API keys to add to group model
* Uses cascader for hierarchical selection
*/
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { Form, Cascader, App, type CascaderProps } from 'antd'; import { Form, Cascader, App, type CascaderProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -10,12 +22,19 @@ import type { ProviderModelItem } from '../../types'
const { SHOW_CHILD } = Cascader; const { SHOW_CHILD } = Cascader;
/**
* Cascader option interface
*/
interface Option { interface Option {
value: string | number; value: string | number;
label: string; label: string;
children?: Option[]; children?: Option[];
[key: string]: any; [key: string]: any;
} }
/**
* Sub-model modal component
*/
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
refresh, refresh,
type, type,
@@ -38,7 +57,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
} }
}, [groupedByProvider, provider]) }, [groupedByProvider, provider])
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
form.resetFields(); form.resetFields();
setVisible(false); setVisible(false);
@@ -46,11 +65,12 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
setModelList([]) setModelList([])
}; };
/** Open modal */
const handleOpen = () => { const handleOpen = () => {
form.resetFields() form.resetFields()
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 /** Save selected models and API keys */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -65,6 +85,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
handleClose() handleClose()
}) })
} }
/** Handle cascader selection change */
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => { const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0]) const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
const lastFilterLit = value.filter(vo => vo.length !== 1) const lastFilterLit = value.filter(vo => vo.length !== 1)
@@ -75,6 +96,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
setSelecteds(selectedOptions) setSelecteds(selectedOptions)
} }
/** Handle provider change and load models */
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => { const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
form.setFieldValue('api_key_ids', undefined) form.setFieldValue('api_key_ids', undefined)
if (provider) { if (provider) {
@@ -110,6 +132,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
setModelList([]) setModelList([])
} }
} }
/** Custom display renderer for cascader */
const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) => const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) =>
labels.map((label, i) => { labels.map((label, i) => {
const option = selectedOptions[i]; const option = selectedOptions[i];
@@ -123,7 +146,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
return <span key={option?.value || i}>{label} / </span>; return <span key={option?.value || i}>{label} / </span>;
}); });
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:12
*/
/**
* Model Implementation Component
* Manages model implementations with API keys for group models
* Allows adding and removing model-API key associations
*/
import { type FC, useRef } from "react"; import { type FC, useRef } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Flex, Button, Space, App } from 'antd' import { Flex, Button, Space, App } from 'antd'
@@ -7,16 +19,27 @@ import SubModelModal from './SubModelModal'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
/**
* Component props
*/
interface ModelImplementProps { interface ModelImplementProps {
/** Model type */
type?: string; type?: string;
/** Current model list value */
value?: any; value?: any;
/** Callback when value changes */
onChange?: (value: any) => void; onChange?: (value: any) => void;
} }
/**
* Model implementation management component
*/
const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => { const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { modal, message } = App.useApp(); const { modal, message } = App.useApp();
const subModelModalRef = useRef<SubModelModalRef>(null) const subModelModalRef = useRef<SubModelModalRef>(null)
/** Open add implementation modal */
const handleAdd = () => { const handleAdd = () => {
if (!type || type.trim() === '') { if (!type || type.trim() === '') {
message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') })) message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') }))
@@ -24,6 +47,7 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
} }
subModelModalRef.current?.handleOpen() subModelModalRef.current?.handleOpen()
} }
/** Delete model implementation */
const handleDelete = (vo: any) => { const handleDelete = (vo: any) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }), title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
@@ -35,6 +59,7 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
} }
}) })
} }
/** Refresh model list after adding implementations */
const handleRefresh = (list: ModelList[]) => { const handleRefresh = (list: ModelList[]) => {
const existingModels = value || []; const existingModels = value || [];
let updatedModels = [...existingModels]; let updatedModels = [...existingModels];
@@ -47,6 +72,7 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
onChange?.([...updatedModels]); onChange?.([...updatedModels]);
} }
/** Group models by provider */
const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => { const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => {
const provider = item.provider || 'unknown'; const provider = item.provider || 'unknown';
if (!acc[provider]) acc[provider] = []; if (!acc[provider]) acc[provider] = [];

View File

@@ -1,17 +1,49 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:24
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:24
*/
/**
* Type definitions for Model Implementation
*/
import type { ModelListItem } from '../../types' import type { ModelListItem } from '../../types'
/**
* Model list item with API key ID
*/
export interface ModelList extends ModelListItem { export interface ModelList extends ModelListItem {
/** Associated API key ID */
api_key_id: string; api_key_id: string;
} }
/**
* Sub-model modal form data
*/
export interface SubModelModalForm { export interface SubModelModalForm {
/** Model provider */
provider: string; provider: string;
/** Selected API key IDs (nested array for cascader) */
api_key_ids: string[][]; api_key_ids: string[][];
} }
/**
* Sub-model modal ref interface
*/
export interface SubModelModalRef { export interface SubModelModalRef {
/** Open modal */
handleOpen: () => void; handleOpen: () => void;
} }
/**
* Sub-model modal props
*/
export interface SubModelModalProps { export interface SubModelModalProps {
/** Model type filter */
type?: string; type?: string;
/** Callback to update model list */
refresh?: (vo: ModelList[]) => void; refresh?: (vo: ModelList[]) => void;
/** Existing models grouped by provider */
groupedByProvider?: Record<string, ModelList[]> groupedByProvider?: Record<string, ModelList[]>
} }

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:45
*/
/**
* Model List Detail Drawer
* Displays detailed list of models from a specific provider
* Allows filtering by type and configuring API keys
*/
import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react'; import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Switch, Row, Col, Space, Tooltip } from 'antd' import { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
@@ -12,10 +24,17 @@ import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
import { getLogoUrl } from '../utils' import { getLogoUrl } from '../utils'
import CustomSelect from '@/components/CustomSelect' import CustomSelect from '@/components/CustomSelect'
/**
* Component props
*/
interface ModelListDetailProps { interface ModelListDetailProps {
/** Callback to refresh parent list */
refresh?: () => void; refresh?: () => void;
} }
/**
* Model list detail drawer component
*/
const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => { const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -25,12 +44,14 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [type, setType] = useState<string | undefined | null>(null) const [type, setType] = useState<string | undefined | null>(null)
/** Open drawer with provider model data */
const handleOpen = (vo: ProviderModelItem) => { const handleOpen = (vo: ProviderModelItem) => {
setType(null) setType(null)
setOpen(true) setOpen(true)
getData(vo) getData(vo)
} }
/** Fetch model data for provider */
const getData = (vo: ProviderModelItem) => { const getData = (vo: ProviderModelItem) => {
if (!vo.provider) return if (!vo.provider) return
@@ -43,9 +64,11 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
setList(response[0].models) setList(response[0].models)
}) })
} }
/** Open key configuration modal */
const handleKeyConfig = (vo: ModelListItem) => { const handleKeyConfig = (vo: ModelListItem) => {
multiKeyConfigModalRef.current?.handleOpen(vo, data.provider) multiKeyConfigModalRef.current?.handleOpen(vo, data.provider)
} }
/** Toggle model active status */
const handleChange = (vo: ModelListItem) => { const handleChange = (vo: ModelListItem) => {
setLoading(true) setLoading(true)
updateModelStatus(vo.id, { is_active: !vo.is_active }) updateModelStatus(vo.id, { is_active: !vo.is_active })
@@ -55,22 +78,27 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
}) })
} }
/** Close drawer */
const handleClose = () => { const handleClose = () => {
setType(null) setType(null)
setOpen(false) setOpen(false)
refresh?.() refresh?.()
} }
/** Refresh model list */
const handleRefresh = () => { const handleRefresh = () => {
getData(data) getData(data)
} }
/** Handle type filter change */
const handleTypeChange = (value: string) => { const handleTypeChange = (value: string) => {
setType(value) setType(value)
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));
/** Filter models by selected type */
const filterList = useMemo(() => { const filterList = useMemo(() => {
if (!type) return list if (!type) return list
return list.filter(vo => vo.type === type) return list.filter(vo => vo.type === type)

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:54:26
*/
/**
* Model Square Detail Drawer
* Displays all models from a specific provider in the model square
* Allows adding models and editing custom models
*/
import { useState, useImperativeHandle, forwardRef } from 'react'; import { useState, useImperativeHandle, forwardRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Space, App, Flex, Tooltip, Divider } from 'antd' import { Button, Space, App, Flex, Tooltip, Divider } from 'antd'
@@ -11,10 +23,19 @@ import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty'; import PageEmpty from '@/components/Empty/PageEmpty';
import { getLogoUrl } from '../utils' import { getLogoUrl } from '../utils'
/**
* Component props
*/
interface ModelSquareDetailProps { interface ModelSquareDetailProps {
/** Callback to refresh parent list */
refresh: () => void; refresh: () => void;
/** Callback to edit model */
handleEdit: (vo: ModelPlazaItem) => void; handleEdit: (vo: ModelPlazaItem) => void;
} }
/**
* Model square detail drawer component
*/
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, ref) => { const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp() const { message } = App.useApp()
@@ -23,15 +44,18 @@ const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProp
const [list, setList] = useState<ModelPlazaItem[]>([]) const [list, setList] = useState<ModelPlazaItem[]>([])
/** Open drawer with model plaza data */
const handleOpen = (vo: ModelPlaza) => { const handleOpen = (vo: ModelPlaza) => {
setModel(vo) setModel(vo)
setOpen(true) setOpen(true)
getList(vo) getList(vo)
} }
/** Close drawer */
const handleClose = () => { const handleClose = () => {
setOpen(false) setOpen(false)
refresh() refresh()
} }
/** Fetch model list for provider */
const getList = (vo: ModelPlaza) => { const getList = (vo: ModelPlaza) => {
getModelPlaza({ provider: vo.provider }) getModelPlaza({ provider: vo.provider })
.then(res => { .then(res => {
@@ -39,6 +63,7 @@ const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProp
setList(response.length > 0 ? response[0].models : []) setList(response.length > 0 ? response[0].models : [])
}) })
} }
/** Add model to workspace */
const handleAdd = (item: ModelPlazaItem) => { const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id) addModelPlaza(item.id)
.then(() => { .then(() => {
@@ -47,6 +72,7 @@ const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProp
}) })
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));

View File

@@ -1,10 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:55
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:55
*/
/**
* Multi-Key Configuration Modal
* Modal for managing multiple API keys for a single model
* Allows adding and removing API keys
*/
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Button } from 'antd'; import { Form, Input, App, Button } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types'; import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types';
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import { addModelApiKey, deleteModelApiKey, getModelInfo } from '@/api/models' import { addModelApiKey, deleteModelApiKey, getModelInfo } from '@/api/models'
/**
* Multi-key configuration modal component
*/
const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => { const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp(); const { message } = App.useApp();
@@ -13,6 +29,7 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
const [form] = Form.useForm<MultiKeyForm>(); const [form] = Form.useForm<MultiKeyForm>();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
/** Close modal and refresh parent */
const handleClose = () => { const handleClose = () => {
setModel({} as ModelListItem); setModel({} as ModelListItem);
refresh?.() refresh?.()
@@ -22,11 +39,13 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
setVisible(false); setVisible(false);
}; };
/** Open modal with model data */
const handleOpen = (vo: ModelListItem) => { const handleOpen = (vo: ModelListItem) => {
setVisible(true); setVisible(true);
getData(vo) getData(vo)
}; };
/** Fetch model information */
const getData = (vo: ModelListItem) => { const getData = (vo: ModelListItem) => {
if (!vo.id) return if (!vo.id) return
@@ -35,6 +54,7 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
setModel(res as ModelListItem) setModel(res as ModelListItem)
}) })
} }
/** Add new API key */
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -58,6 +78,7 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
console.log('err', err) console.log('err', err)
}); });
} }
/** Delete API key */
const handleDelete = (api_key_id: string) => { const handleDelete = (api_key_id: string) => {
deleteModelApiKey(api_key_id) deleteModelApiKey(api_key_id)
.then(() => { .then(() => {
@@ -66,6 +87,7 @@ const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigMod
}) })
} }
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));

View File

@@ -1,3 +1,15 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:05
*/
/**
* Model Management Main Page
* Manages AI models with three views: group models, model list, and model square
* Supports filtering, searching, and CRUD operations
*/
import { useState, useRef, type FC } from 'react'; import { useState, useRef, type FC } from 'react';
import { Button, Flex, Space, type SegmentedProps, Form } from 'antd' import { Button, Flex, Space, type SegmentedProps, Form } from 'antd'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -13,8 +25,14 @@ import CustomModelModal from './components/CustomModelModal'
import CustomSelect from '@/components/CustomSelect' import CustomSelect from '@/components/CustomSelect'
import { modelTypeUrl, modelProviderUrl } from '@/api/models' import { modelTypeUrl, modelProviderUrl } from '@/api/models'
/**
* Available tab keys
*/
const tabKeys = ['group', 'list', 'square'] const tabKeys = ['group', 'list', 'square']
const ModelManagement: FC = () => {
/**
* Model management main component
*/const ModelManagement: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('group'); const [activeTab, setActiveTab] = useState('group');
const configModalRef = useRef<GroupModelModalRef>(null) const configModalRef = useRef<GroupModelModalRef>(null)
@@ -24,17 +42,20 @@ const ModelManagement: FC = () => {
const [form] = Form.useForm<Query>() const [form] = Form.useForm<Query>()
const query = Form.useWatch([], form) const query = Form.useWatch([], form)
/** Format tab items with translations */
const formatTabItems = () => { const formatTabItems = () => {
return tabKeys.map(value => ({ return tabKeys.map(value => ({
value, value,
label: t(`modelNew.${value}`), label: t(`modelNew.${value}`),
})) }))
} }
/** Handle tab change */
const handleChangeTab = (value: SegmentedProps['value']) => { const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string); setActiveTab(value as string);
form.resetFields() form.resetFields()
} }
/** Open edit modal based on active tab */
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => { const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
switch(activeTab) { switch(activeTab) {
case 'group': case 'group':
@@ -45,6 +66,7 @@ const ModelManagement: FC = () => {
break break
} }
} }
/** Refresh list based on active tab */
const handleRefresh = () => { const handleRefresh = () => {
switch (activeTab) { switch (activeTab) {
case 'group': case 'group':

View File

@@ -1,139 +1,315 @@
export interface Query { /*
type?: string; * @Author: ZhaoYing
provider?: string; * @Date: 2026-02-03 16:50:18
is_active?: boolean; * @Last Modified by: ZhaoYing
is_public?: boolean; * @Last Modified time: 2026-02-03 16:50:18
is_composite?: boolean; */
search?: string; /**
* Type definitions for Model Management
*/
/**
* Query parameters for model filtering
*/
export interface Query {
/** Model type filter */
type?: string;
/** Model provider filter */
provider?: string;
/** Active status filter */
is_active?: boolean;
/** Public status filter */
is_public?: boolean;
/** Composite model filter */
is_composite?: boolean;
/** Search keyword */
search?: string;
/** Page size */
pagesize?: number; pagesize?: number;
/** Page number */
page?: number; page?: number;
} }
/**
* Description item for model details
*/
export interface DescriptionItem { export interface DescriptionItem {
/** Item key */
key: string; key: string;
/** Item label */
label: string; label: string;
/** Item content */
children: string; children: string;
} }
/**
* Composite model form data
*/
export interface CompositeModelForm { export interface CompositeModelForm {
/** Model logo */
logo?: any; logo?: any;
/** Model name */
name: string; name: string;
/** Model type */
type?: string; type?: string;
/** Model description */
description: string; description: string;
/** Associated API key IDs */
api_key_ids: ModelApiKey[] | string[]; api_key_ids: ModelApiKey[] | string[];
} }
/**
* Group model modal ref interface
*/
export interface GroupModelModalRef { export interface GroupModelModalRef {
/** Open modal with optional model data */
handleOpen: (model?: ModelListItem) => void; handleOpen: (model?: ModelListItem) => void;
} }
/**
* Group model modal props
*/
export interface GroupModelModalProps { export interface GroupModelModalProps {
/** Callback to refresh model list */
refresh?: () => void; refresh?: () => void;
} }
/**
* Model list detail ref interface
*/
export interface ModelListDetailRef { export interface ModelListDetailRef {
/** Open detail drawer with provider model data */
handleOpen: (vo: ProviderModelItem) => void; handleOpen: (vo: ProviderModelItem) => void;
} }
/**
* Model API key configuration
*/
export interface ModelApiKey { export interface ModelApiKey {
/** Model name */
model_name: string; model_name: string;
/** API key description */
description: string | null; description: string | null;
/** Model provider */
provider: string; provider: string;
/** API key value */
api_key: string; api_key: string;
/** API base URL */
api_base: string; api_base: string;
/** Additional configuration */
config: any; config: any;
/** Whether API key is active */
is_active: boolean; is_active: boolean;
/** Priority level */
priority: string; priority: string;
/** API key ID */
id: string; id: string;
/** Usage count */
usage_count: string; usage_count: string;
/** Last used timestamp */
last_used_at: number; last_used_at: number;
/** Creation timestamp */
created_at: number; created_at: number;
/** Update timestamp */
updated_at: number; updated_at: number;
/** Associated model config IDs */
model_config_ids: string[]; model_config_ids: string[];
} }
/**
* Model list item data structure
*/
export interface ModelListItem { export interface ModelListItem {
/** Model name */
model_name?: string; model_name?: string;
/** Associated model config IDs */
model_config_ids: string[]; model_config_ids: string[];
/** Display name */
name: string; name: string;
/** Model type */
type: string; type: string;
/** Model logo URL */
logo: string; logo: string;
/** Model description */
description: string; description: string;
/** Model provider */
provider: string; provider: string;
/** Model configuration */
config: any; config: any;
/** Whether model is active */
is_active: boolean; is_active: boolean;
/** Whether model is public */
is_public: boolean; is_public: boolean;
/** Model ID */
id: string; id: string;
/** Creation timestamp */
created_at: number; created_at: number;
/** Update timestamp */
updated_at: number; updated_at: number;
/** Associated API keys */
api_keys: ModelApiKey[] api_keys: ModelApiKey[]
} }
/**
* Provider model item grouping
*/
export interface ProviderModelItem { export interface ProviderModelItem {
/** Provider name */
provider: string; provider: string;
/** Provider logo URL */
logo?: string; logo?: string;
/** Provider tags */
tags: string[]; tags: string[];
/** Models from this provider */
models: ModelListItem[]; models: ModelListItem[];
} }
/**
* Key configuration modal form data
*/
export interface KeyConfigModalForm { export interface KeyConfigModalForm {
/** Model provider */
provider: string; provider: string;
/** API key value */
api_key: string; api_key: string;
/** API base URL */
api_base: string; api_base: string;
} }
/**
* Key configuration modal ref interface
*/
export interface KeyConfigModalRef { export interface KeyConfigModalRef {
/** Open modal with provider model data */
handleOpen: (vo: ProviderModelItem) => void; handleOpen: (vo: ProviderModelItem) => void;
} }
/**
* Key configuration modal props
*/
export interface KeyConfigModalProps { export interface KeyConfigModalProps {
/** Callback to refresh model list */
refresh?: () => void; refresh?: () => void;
} }
/**
* Multi-key configuration form data
*/
export interface MultiKeyForm { export interface MultiKeyForm {
/** Model config ID */
model_config_id?: string; model_config_id?: string;
/** Model name */
model_name: string; model_name: string;
/** Model provider */
provider: string; provider: string;
/** API key value */
api_key: string; api_key: string;
/** API base URL */
api_base: string; api_base: string;
} }
/**
* Multi-key configuration modal ref interface
*/
export interface MultiKeyConfigModalRef { export interface MultiKeyConfigModalRef {
/** Open modal with model data */
handleOpen: (vo: ModelListItem, provider?: string) => void; handleOpen: (vo: ModelListItem, provider?: string) => void;
} }
/**
* Multi-key configuration modal props
*/
export interface MultiKeyConfigModalProps { export interface MultiKeyConfigModalProps {
/** Callback to refresh model list */
refresh?: () => void; refresh?: () => void;
} }
/**
* Model plaza grouping by provider
*/
export interface ModelPlaza { export interface ModelPlaza {
/** Provider name */
provider: string; provider: string;
/** Models from this provider */
models: ModelPlazaItem[]; models: ModelPlazaItem[];
} }
/**
* Model plaza item data structure
*/
export interface ModelPlazaItem { export interface ModelPlazaItem {
/** Model ID */
id: string; id: string;
/** Model name */
name: string; name: string;
/** Model type */
type: string; type: string;
/** Model provider */
provider: string; provider: string;
/** Model logo URL */
logo: string; logo: string;
/** Model description */
description: string; description: string;
/** Whether model is deprecated */
is_deprecated: boolean; is_deprecated: boolean;
/** Whether model is official */
is_official: boolean; is_official: boolean;
/** Model tags */
tags: string[]; tags: string[];
/** Number of times added */
add_count: number; add_count: number;
/** Whether user has added this model */
is_added: boolean; is_added: boolean;
} }
/**
* Model square detail ref interface
*/
export interface ModelSquareDetailRef { export interface ModelSquareDetailRef {
/** Open detail drawer with model plaza data */
handleOpen: (vo: ModelPlaza) => void; handleOpen: (vo: ModelPlaza) => void;
} }
/**
* Custom model form data
*/
export interface CustomModelForm { export interface CustomModelForm {
/** Model name */
name: string; name: string;
/** Model type */
type?: string; type?: string;
/** Model provider */
provider?: string; provider?: string;
/** Model logo */
logo?: any; logo?: any;
/** Model description */
description: string; description: string;
/** Whether model is official */
is_official: boolean; is_official: boolean;
/** Model tags */
tags: string[]; tags: string[];
} }
/**
* Custom model modal ref interface
*/
export interface CustomModelModalRef { export interface CustomModelModalRef {
/** Open modal with optional model plaza item */
handleOpen: (vo?: ModelPlazaItem) => void; handleOpen: (vo?: ModelPlazaItem) => void;
} }
/**
* Custom model modal props
*/
export interface CustomModelModalProps { export interface CustomModelModalProps {
/** Callback to refresh model list */
refresh?: () => void; refresh?: () => void;
} }
/**
* Base ref interface for list components
*/
export interface BaseRef { export interface BaseRef {
/** Refresh list data */
getList: () => void; getList: () => void;
} }

View File

@@ -1,3 +1,13 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:22
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:50:22
*/
/**
* Utility functions for Model Management
*/
import bedrockIcon from '@/assets/images/model/bedrock.svg' import bedrockIcon from '@/assets/images/model/bedrock.svg'
import dashscopeIcon from '@/assets/images/model/dashscope.png' import dashscopeIcon from '@/assets/images/model/dashscope.png'
import gpustackIcon from '@/assets/images/model/gpustack.png' import gpustackIcon from '@/assets/images/model/gpustack.png'
@@ -5,6 +15,9 @@ import ollamaIcon from '@/assets/images/model/ollama.svg'
import openaiIcon from '@/assets/images/model/openai.svg' import openaiIcon from '@/assets/images/model/openai.svg'
import xinferenceIcon from '@/assets/images/model/xinference.svg' import xinferenceIcon from '@/assets/images/model/xinference.svg'
/**
* Provider icon mapping
*/
export const ICONS = { export const ICONS = {
bedrock: bedrockIcon, bedrock: bedrockIcon,
dashscope: dashscopeIcon, dashscope: dashscopeIcon,
@@ -14,6 +27,11 @@ export const ICONS = {
xinference: xinferenceIcon xinference: xinferenceIcon
} }
/**
* Get logo URL from provider name or URL
* @param logo - Provider name or logo URL
* @returns Logo URL or undefined
*/
export const getLogoUrl = (logo?: string) => { export const getLogoUrl = (logo?: string) => {
if (!logo) { if (!logo) {
return undefined return undefined

View File

@@ -1,4 +1,16 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:34:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:34:18
*/
/**
* No Permission Page
* Displays when user lacks access rights to a resource
*/
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import noPermission from '@/assets/images/empty/noPermission.png'; import noPermission from '@/assets/images/empty/noPermission.png';
import Empty from '@/components/Empty'; import Empty from '@/components/Empty';

View File

@@ -1,4 +1,16 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:35:01
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:35:01
*/
/**
* Not Found Page (404)
* Displays when requested route or resource does not exist
*/
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import notFoundImg from '@/assets/images/empty/404.png'; import notFoundImg from '@/assets/images/empty/404.png';
import Empty from '@/components/Empty'; import Empty from '@/components/Empty';

View File

@@ -1,3 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:35:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:35:49
*/
/**
* Order Detail Component
* Modal displaying detailed order information including payment details
*/
import { forwardRef, useImperativeHandle, useState, useCallback } from 'react'; import { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import { Descriptions } from 'antd'; import { Descriptions } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -14,11 +25,12 @@ const OrderDetail = forwardRef<OrderDetailRef, { getProductType: (type: string)
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [data, setData] = useState({}) const [data, setData] = useState({})
// 封装取消方法,添加关闭弹窗逻辑 /** Close modal */
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
}; };
/** Open modal and fetch order details */
const handleOpen = (order: Order) => { const handleOpen = (order: Order) => {
setVisible(true); setVisible(true);
getOrderDetail(order.order_no) getOrderDetail(order.order_no)
@@ -26,6 +38,7 @@ const OrderDetail = forwardRef<OrderDetailRef, { getProductType: (type: string)
setData(res as Order) setData(res as Order)
}) })
}; };
/** Format order information items */
const formatItems = useCallback(() => { const formatItems = useCallback(() => {
if (!data) return [] if (!data) return []
return ['order_no', 'product_type', 'payable_amount', 'status', 'pay_time', 'create_time'].map(key => { return ['order_no', 'product_type', 'payable_amount', 'status', 'pay_time', 'create_time'].map(key => {
@@ -43,6 +56,7 @@ const OrderDetail = forwardRef<OrderDetailRef, { getProductType: (type: string)
} }
}) })
}, [data]) }, [data])
/** Format payment information items */
const formatPayItems = useCallback(() => { const formatPayItems = useCallback(() => {
if (!data) return [] if (!data) return []
return ['pay_txn_id', 'payer'].map(key => ({ return ['pay_txn_id', 'payer'].map(key => ({
@@ -52,13 +66,12 @@ const OrderDetail = forwardRef<OrderDetailRef, { getProductType: (type: string)
})) }))
}, [data]) }, [data])
// 暴露给父组件的方法 /** Expose methods to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
})); }));
// ['pay_txn_id', 'payer']
// ['pay_txn_id', 'payer']
return ( return (
<RbModal <RbModal
title={t('pricing.orderDetail')} title={t('pricing.orderDetail')}

Some files were not shown because too many files have changed in this diff Show More