Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
@@ -267,25 +267,27 @@ async def delete_file(
|
|||||||
async def get_file_url(
|
async def get_file_url(
|
||||||
file_id: uuid.UUID,
|
file_id: uuid.UUID,
|
||||||
expires: int = None,
|
expires: int = None,
|
||||||
|
permanent: bool = False,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
storage_service: FileStorageService = Depends(get_file_storage_service),
|
storage_service: FileStorageService = Depends(get_file_storage_service),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get a temporary access URL for a file (no authentication required).
|
Get an access URL for a file (no authentication required).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_id: The UUID of the file.
|
file_id: The UUID of the file.
|
||||||
expires: URL validity period in seconds (default from FILE_URL_EXPIRES env).
|
expires: URL validity period in seconds (default from FILE_URL_EXPIRES env).
|
||||||
|
permanent: If True, return a permanent URL without expiration.
|
||||||
db: Database session.
|
db: Database session.
|
||||||
storage_service: The file storage service.
|
storage_service: The file storage service.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ApiResponse with the temporary access URL.
|
ApiResponse with the access URL.
|
||||||
"""
|
"""
|
||||||
if expires is None:
|
if expires is None:
|
||||||
expires = settings.FILE_URL_EXPIRES
|
expires = settings.FILE_URL_EXPIRES
|
||||||
|
|
||||||
api_logger.info(f"Get file URL request: file_id={file_id}, expires={expires}")
|
api_logger.info(f"Get file URL request: file_id={file_id}, expires={expires}, permanent={permanent}")
|
||||||
|
|
||||||
# Query file metadata from database
|
# Query file metadata from database
|
||||||
file_metadata = db.query(FileMetadata).filter(FileMetadata.id == file_id).first()
|
file_metadata = db.query(FileMetadata).filter(FileMetadata.id == file_id).first()
|
||||||
@@ -306,6 +308,20 @@ async def get_file_url(
|
|||||||
storage = storage_service.storage
|
storage = storage_service.storage
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if permanent:
|
||||||
|
# Generate permanent URL (no expiration check)
|
||||||
|
server_url = f"http://{settings.SERVER_IP}:8000/api"
|
||||||
|
url = f"{server_url}/storage/permanent/{file_id}"
|
||||||
|
return success(
|
||||||
|
data={
|
||||||
|
"url": url,
|
||||||
|
"expires_in": None,
|
||||||
|
"permanent": True,
|
||||||
|
"file_name": file_metadata.file_name,
|
||||||
|
},
|
||||||
|
msg="Permanent file URL generated successfully"
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(storage, LocalStorage):
|
if isinstance(storage, LocalStorage):
|
||||||
# For local storage, generate signed URL with expiration
|
# For local storage, generate signed URL with expiration
|
||||||
url = generate_signed_url(str(file_id), expires)
|
url = generate_signed_url(str(file_id), expires)
|
||||||
@@ -318,6 +334,7 @@ async def get_file_url(
|
|||||||
data={
|
data={
|
||||||
"url": url,
|
"url": url,
|
||||||
"expires_in": expires,
|
"expires_in": expires,
|
||||||
|
"permanent": False,
|
||||||
"file_name": file_metadata.file_name,
|
"file_name": file_metadata.file_name,
|
||||||
},
|
},
|
||||||
msg="File URL generated successfully"
|
msg="File URL generated successfully"
|
||||||
@@ -410,3 +427,73 @@ async def public_download_file(
|
|||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"Failed to retrieve file: {str(e)}"
|
detail=f"Failed to retrieve file: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/permanent/{file_id}", response_model=Any)
|
||||||
|
async def permanent_download_file(
|
||||||
|
file_id: uuid.UUID,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
storage_service: FileStorageService = Depends(get_file_storage_service),
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Permanent file download endpoint (no expiration, no signature required).
|
||||||
|
|
||||||
|
This endpoint allows downloading files without authentication or expiration.
|
||||||
|
Use with caution as URLs are permanently accessible.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_id: The UUID of the file.
|
||||||
|
db: Database session.
|
||||||
|
storage_service: The file storage service.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FileResponse for the requested file.
|
||||||
|
"""
|
||||||
|
api_logger.info(f"Permanent download request: file_id={file_id}")
|
||||||
|
|
||||||
|
# Query file metadata from database
|
||||||
|
file_metadata = db.query(FileMetadata).filter(FileMetadata.id == file_id).first()
|
||||||
|
if not file_metadata:
|
||||||
|
api_logger.warning(f"File not found in database: file_id={file_id}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="The file does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_metadata.status != "completed":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"File upload not completed, status: {file_metadata.status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_key = file_metadata.file_key
|
||||||
|
storage = storage_service.storage
|
||||||
|
|
||||||
|
if isinstance(storage, LocalStorage):
|
||||||
|
full_path = storage._get_full_path(file_key)
|
||||||
|
|
||||||
|
if not full_path.exists():
|
||||||
|
api_logger.warning(f"File not found on disk: file_key={file_key}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="File not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
api_logger.info(f"Serving permanent file: file_key={file_key}")
|
||||||
|
return FileResponse(
|
||||||
|
path=str(full_path),
|
||||||
|
filename=file_metadata.file_name,
|
||||||
|
media_type=file_metadata.content_type or "application/octet-stream"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For remote storage, redirect to presigned URL with long expiration
|
||||||
|
try:
|
||||||
|
# Use a very long expiration (7 days max for most cloud providers)
|
||||||
|
presigned_url = await storage_service.get_file_url(file_key, expires=604800)
|
||||||
|
return RedirectResponse(url=presigned_url, status_code=status.HTTP_302_FOUND)
|
||||||
|
except Exception as e:
|
||||||
|
api_logger.error(f"Failed to get presigned URL: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to retrieve file: {str(e)}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ dependencies = [
|
|||||||
"deprecated>=1.3.1",
|
"deprecated>=1.3.1",
|
||||||
"oss2>=2.19.1",
|
"oss2>=2.19.1",
|
||||||
"flower>=2.0.1",
|
"flower>=2.0.1",
|
||||||
"aiofile>=3.9.0",
|
"aiofiles>=23.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
@@ -28,15 +28,19 @@ import 'dayjs/locale/zh-cn'
|
|||||||
import 'dayjs/plugin/timezone'
|
import 'dayjs/plugin/timezone'
|
||||||
import 'dayjs/plugin/utc'
|
import 'dayjs/plugin/utc'
|
||||||
import { cookieUtils } from './utils/request';
|
import { cookieUtils } from './utils/request';
|
||||||
|
import { useUser } from '@/store/user';
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { locale, language, timeZone } = useI18n()
|
const { locale, language, timeZone } = useI18n()
|
||||||
|
const { checkJump } = useUser();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authToken = cookieUtils.get('authToken')
|
const authToken = cookieUtils.get('authToken')
|
||||||
if (!authToken && !window.location.hash.includes('#/login') && !window.location.hash.includes('#/conversation/')) {
|
if (!authToken && !window.location.hash.includes('#/login') && !window.location.hash.includes('#/conversation/')) {
|
||||||
window.location.href = `/#/login`;
|
window.location.href = `/#/login`;
|
||||||
|
} else {
|
||||||
|
checkJump()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -2064,6 +2064,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
stability: 'Stability',
|
stability: 'Stability',
|
||||||
resilience: 'Resilience',
|
resilience: 'Resilience',
|
||||||
suggestions: 'Personalized Suggestions',
|
suggestions: 'Personalized Suggestions',
|
||||||
|
suggestionLoading: 'Your personalized suggestions are being generated',
|
||||||
},
|
},
|
||||||
reflectionEngine: {
|
reflectionEngine: {
|
||||||
reflectionEngineConfig: 'Reflection Engine Configuration',
|
reflectionEngineConfig: 'Reflection Engine Configuration',
|
||||||
|
|||||||
@@ -716,7 +716,7 @@ export const zh = {
|
|||||||
total_models: '可用模型总数',
|
total_models: '可用模型总数',
|
||||||
total_spaces: '活跃空间数量',
|
total_spaces: '活跃空间数量',
|
||||||
total_users: '用户总数',
|
total_users: '用户总数',
|
||||||
total_running_apps: '应用运行次数',
|
total_running_apps: '正在运行的应用',
|
||||||
desc_models: '包含 {{ account }} 个大语言模型和 {{ nums }} 个嵌入模型',
|
desc_models: '包含 {{ account }} 个大语言模型和 {{ nums }} 个嵌入模型',
|
||||||
desc_spaces: '多于上周',
|
desc_spaces: '多于上周',
|
||||||
desc_users: '本周新增',
|
desc_users: '本周新增',
|
||||||
@@ -2158,6 +2158,7 @@ export const zh = {
|
|||||||
stability: '稳定性',
|
stability: '稳定性',
|
||||||
resilience: '恢复力',
|
resilience: '恢复力',
|
||||||
suggestions: '个性化建议',
|
suggestions: '个性化建议',
|
||||||
|
suggestionLoading: '您的个性化建议正在生成中',
|
||||||
},
|
},
|
||||||
reflectionEngine: {
|
reflectionEngine: {
|
||||||
reflectionEngineConfig: '反思引擎配置',
|
reflectionEngineConfig: '反思引擎配置',
|
||||||
|
|||||||
@@ -21,8 +21,15 @@ export interface UserState {
|
|||||||
clearUserInfo: () => void;
|
clearUserInfo: () => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
getStorageType: () => void;
|
getStorageType: () => void;
|
||||||
|
checkJump: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const whitePage = [
|
||||||
|
'/conversation',
|
||||||
|
'/login',
|
||||||
|
'/invite-register'
|
||||||
|
]
|
||||||
|
|
||||||
export const useUser = create<UserState>((set, get) => ({
|
export const useUser = create<UserState>((set, get) => ({
|
||||||
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User,
|
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User,
|
||||||
loginInfo: {} as LoginInfo,
|
loginInfo: {} as LoginInfo,
|
||||||
@@ -36,8 +43,10 @@ export const useUser = create<UserState>((set, get) => ({
|
|||||||
if (!cookieUtils.get('authToken')) {
|
if (!cookieUtils.get('authToken')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const { checkJump } = get()
|
||||||
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
|
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
|
||||||
if (localUser.id) {
|
if (localUser.id) {
|
||||||
|
checkJump()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getUsers()
|
getUsers()
|
||||||
@@ -87,5 +96,14 @@ export const useUser = create<UserState>((set, get) => ({
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error('Failed to load storage type');
|
console.error('Failed to load storage type');
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
checkJump: () => {
|
||||||
|
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
|
||||||
|
const hash = window.location.hash;
|
||||||
|
|
||||||
|
if (localUser.id && (!localUser.current_workspace_id || localUser.current_workspace_id === '') && !whitePage.find(vo => hash.includes(vo))) {
|
||||||
|
console.log('whitePage', whitePage.find(vo => hash.includes(vo)))
|
||||||
|
window.location.href = '/#/index'
|
||||||
|
}
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
@@ -52,6 +52,8 @@ service.interceptors.request.use(
|
|||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const language = localStorage.getItem('language')
|
||||||
|
config.headers['X-Language-Type'] = language || 'en';
|
||||||
config.headers.Cookie = undefined
|
config.headers.Cookie = undefined
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
@@ -146,7 +148,7 @@ service.interceptors.response.use(
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (!msg && Array.isArray(error.response?.data?.detail)) {
|
if (!msg && Array.isArray(error.response?.data?.detail)) {
|
||||||
msg = error.response?.data?.detail?.map(item => item.msg).join(';')
|
msg = error.response?.data?.detail?.map((item: { msg: string }) => item.msg).join(';')
|
||||||
} else {
|
} else {
|
||||||
msg = msg || i18n.t('common.unknownError');
|
msg = msg || i18n.t('common.unknownError');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
|||||||
let allTools = Array.isArray(response.tools) ? response.tools : []
|
let allTools = Array.isArray(response.tools) ? response.tools : []
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...response,
|
...response,
|
||||||
tools: allTools
|
tools: allTools,
|
||||||
|
memory: {
|
||||||
|
...response.memory,
|
||||||
|
memory_content: response.memory?.memory_content ? Number(response.memory?.memory_content) : undefined
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setData({
|
setData({
|
||||||
...response,
|
...response,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo,useRef, useState, useEffect } from 'react';
|
import { useMemo,useRef, useState, useEffect } from 'react';
|
||||||
import { Button, Flex, Radio, Steps, Modal, Input, Spin, message, Checkbox, Select, Form} from 'antd';
|
import { Button, Flex, Radio, Steps, Modal, Input, Spin, message, Checkbox, Select, Form, Progress} from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import Table, { type TableRef } from '@/components/Table'
|
import Table, { type TableRef } from '@/components/Table'
|
||||||
@@ -261,12 +261,36 @@ const CreateDataset = () => {
|
|||||||
dataIndex: 'progress',
|
dataIndex: 'progress',
|
||||||
key: 'progress',
|
key: 'progress',
|
||||||
render: (value: number, record: any) => {
|
render: (value: number, record: any) => {
|
||||||
return (
|
// value = 1 时完成,0~1 时显示进度条
|
||||||
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
|
if (value === 1) {
|
||||||
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: value === 1 ? '#369F21' : '#FF8A4C' }}></span>
|
return (
|
||||||
<span>{value === 1 ? t('knowledgeBase.completed') : value === 0 ? t('knowledgeBase.pending') : t('knowledgeBase.processing')}</span>
|
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
|
||||||
</span>
|
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: '#369F21' }}></span>
|
||||||
);
|
<span>{t('knowledgeBase.completed')}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (value > 0 && value < 1) {
|
||||||
|
// 处理中,显示进度条
|
||||||
|
return (
|
||||||
|
<div className="rb:flex rb:items-center rb:gap-2">
|
||||||
|
<Progress
|
||||||
|
percent={Math.round(value * 100)}
|
||||||
|
size="small"
|
||||||
|
status="active"
|
||||||
|
strokeColor="#1677ff"
|
||||||
|
style={{ width: '120px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// value = 0 或其他情况,显示待处理
|
||||||
|
return (
|
||||||
|
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
|
||||||
|
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: '#FF8A4C' }}></span>
|
||||||
|
<span>{t('knowledgeBase.pending')}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -553,21 +577,24 @@ const CreateDataset = () => {
|
|||||||
if (abortController) {
|
if (abortController) {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
abortControllersRef.current.delete(fileUid);
|
abortControllersRef.current.delete(fileUid);
|
||||||
|
console.log('已取消上传:', (file as any).name);
|
||||||
}
|
}
|
||||||
console.log('文件移除前:', uploadRef.current?.fileList);
|
|
||||||
// 如果文件已经上传成功,删除服务器上的文件并从rechunkFileIds中移除对应的ID
|
// 只有当文件已经上传成功(有response.id)时,才删除服务器上的文件
|
||||||
if (file.response?.id) {
|
if (file.response?.id) {
|
||||||
try {
|
try {
|
||||||
await deleteDocument(file.response.id);
|
await deleteDocument(file.response.id);
|
||||||
setRechunkFileIds(prev => prev.filter(id => id !== file.response.id));
|
setRechunkFileIds(prev => prev.filter(id => id !== file.response.id));
|
||||||
|
console.log('已删除服务器文件:', file.response.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除文件失败:', error);
|
console.error('删除文件失败:', error);
|
||||||
messageApi.error('删除文件失败');
|
messageApi.error(t('common.deleteFailed') || '删除文件失败');
|
||||||
|
return false; // 删除失败时不移除文件
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; // 允许移除文件
|
// 允许移除文件(无论是取消上传还是删除成功)
|
||||||
|
return true;
|
||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
{source && source === 'link' && (
|
{source && source === 'link' && (
|
||||||
@@ -776,7 +803,7 @@ const CreateDataset = () => {
|
|||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{current === 2 && (
|
{current === 2 && (
|
||||||
<Spin spinning={pollingLoading} tip={t('knowledgeBase.processingDocuments') || '正在处理文档...'}>
|
// <Spin spinning={pollingLoading} tip={t('knowledgeBase.processingDocuments') || '正在处理文档...'}>
|
||||||
<div className='rb:text-sm rb:text-gray-500 rb:mt-4 rb:h-[calc(100%-160px)] rb:overflow-y-auto'>
|
<div className='rb:text-sm rb:text-gray-500 rb:mt-4 rb:h-[calc(100%-160px)] rb:overflow-y-auto'>
|
||||||
{rechunkFileIds.length > 0 ? (
|
{rechunkFileIds.length > 0 ? (
|
||||||
<Table
|
<Table
|
||||||
@@ -797,7 +824,7 @@ const CreateDataset = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
// </Spin>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`rb:flex rb:gap-3 rb:mt-6 ${current === 1 || (source == 'link' && current === 0) || (source == 'text' && current === 0) ? 'rb:pl-40 rb:mt-10' : ''}`}>
|
<div className={`rb:flex rb:gap-3 rb:mt-6 ${current === 1 || (source == 'link' && current === 0) || (source == 'text' && current === 0) ? 'rb:pl-40 rb:mt-10' : ''}`}>
|
||||||
@@ -810,7 +837,6 @@ const CreateDataset = () => {
|
|||||||
type='primary'
|
type='primary'
|
||||||
onClick={current === 2 ? handleStartUpload : handleNext}
|
onClick={current === 2 ? handleStartUpload : handleNext}
|
||||||
disabled={pollingLoading || (current === 0 && rechunkFileIds.length === 0)}
|
disabled={pollingLoading || (current === 0 && rechunkFileIds.length === 0)}
|
||||||
loading={pollingLoading}
|
|
||||||
>
|
>
|
||||||
{current === 2 ? t('knowledgeBase.startUploading') || 'Start Upload' : t('common.next') || 'Next'}
|
{current === 2 ? t('knowledgeBase.startUploading') || 'Start Upload' : t('common.next') || 'Next'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -164,6 +164,26 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setModelOptionsByType(next);
|
setModelOptionsByType(next);
|
||||||
|
|
||||||
|
// 如果不是编辑模式,为每个类型的下拉框设置默认值为第一条数据
|
||||||
|
if (!datasets?.id) {
|
||||||
|
const defaultValues: Record<string, string> = {};
|
||||||
|
types.forEach((tp) => {
|
||||||
|
const fieldKey = typeToFieldKey(tp);
|
||||||
|
const options = tp.toLowerCase() === 'llm'
|
||||||
|
? [...(next['llm'] || []), ...(next['chat'] || [])]
|
||||||
|
: next[tp] || [];
|
||||||
|
|
||||||
|
// 如果有选项且当前字段没有值,设置第一个选项为默认值
|
||||||
|
if (options.length > 0 && !form.getFieldValue(fieldKey)) {
|
||||||
|
defaultValues[fieldKey] = options[0].value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(defaultValues).length > 0) {
|
||||||
|
form.setFieldsValue(defaultValues as Partial<KnowledgeBaseFormData>);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBaseFields = (record: KnowledgeBaseListItem | null, type?: string) => {
|
const setBaseFields = (record: KnowledgeBaseListItem | null, type?: string) => {
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
|||||||
: Object.keys(data).length > 0
|
: Object.keys(data).length > 0
|
||||||
? <Space size={16} direction="vertical" className="rb:w-full">
|
? <Space size={16} direction="vertical" className="rb:w-full">
|
||||||
{['memory_insight', 'key_findings', 'behavior_pattern', 'growth_trajectory'].map(key => {
|
{['memory_insight', 'key_findings', 'behavior_pattern', 'growth_trajectory'].map(key => {
|
||||||
if (data[key as keyof Data]) {
|
const value = data[key as keyof Data];
|
||||||
|
if (Array.isArray(value) && value.length > 0 || (!Array.isArray(value) && value)) {
|
||||||
return (
|
return (
|
||||||
<div key={key} className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:text-[#5B6167] rb:leading-5">
|
<div key={key} className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:text-[#5B6167] rb:leading-5">
|
||||||
<div className={clsx(`rb:relative rb:before:content-[''] rb:before:block rb:before:h-4 rb:before:absolute rb:before:top-0.5 rb:before:left-0 rb:before:w-1 rb:pl-4 rb:mb-2 rb:font-medium rb:leading-5`, {
|
<div className={clsx(`rb:relative rb:before:content-[''] rb:before:block rb:before:h-4 rb:before:absolute rb:before:top-0.5 rb:before:left-0 rb:before:w-1 rb:pl-4 rb:mb-2 rb:font-medium rb:leading-5`, {
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ const PageHeader: FC<ConfigHeaderProps> = ({
|
|||||||
{operation}
|
{operation}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space size={12}>
|
<div className="rb:flex rb:items-center rb:gap-3">
|
||||||
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={goBack}>
|
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={goBack}>
|
||||||
<img src={logoutIcon} className="rb:w-4 rb:h-4" />
|
<img src={logoutIcon} className="rb:w-4 rb:h-4" />
|
||||||
{t('common.return')}
|
{t('common.return')}
|
||||||
</Button>
|
</Button>
|
||||||
{extra}
|
{extra}
|
||||||
</Space>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ const RelationshipNetwork:FC = () => {
|
|||||||
size={[197.81, 150]}
|
size={[197.81, 150]}
|
||||||
/>
|
/>
|
||||||
: <>
|
: <>
|
||||||
<div className="rb:bg-[#F6F8FC] rb:border-t rb:border-b rb:border-[#DFE4ED] rb:font-medium rb:py-2 rb:px-4 rb:h-10">{selectedNode.name}</div>
|
{selectedNode.name && <div className="rb:bg-[#F6F8FC] rb:border-t rb:border-b rb:border-[#DFE4ED] rb:font-medium rb:py-2 rb:px-4 rb:h-10">{selectedNode.name}</div>}
|
||||||
<div className="rb:p-4">
|
<div className="rb:p-4">
|
||||||
<>
|
<>
|
||||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.memoryContent')}</div>
|
<div className="rb:font-medium rb:leading-5">{t('userMemory.memoryContent')}</div>
|
||||||
@@ -297,7 +297,8 @@ const RelationshipNetwork:FC = () => {
|
|||||||
{selectedNode.label === 'Statement' && <>
|
{selectedNode.label === 'Statement' && <>
|
||||||
{(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => {
|
{(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => {
|
||||||
const statementProps = selectedNode.properties as StatementNodeProperties;
|
const statementProps = selectedNode.properties as StatementNodeProperties;
|
||||||
if ((key === 'emotion_keywords' && statementProps[key]?.length > 0) || statementProps[key]) {
|
if ((key === 'emotion_keywords' && statementProps[key]?.length > 0) || typeof statementProps[key] === 'string') {
|
||||||
|
console.log('statementProps[key]', statementProps[key])
|
||||||
return (
|
return (
|
||||||
<div className="rb:mt-4" key={key}>
|
<div className="rb:mt-4" key={key}>
|
||||||
{t(`userMemory.Statement_${key}`)}
|
{t(`userMemory.Statement_${key}`)}
|
||||||
@@ -321,7 +322,10 @@ const RelationshipNetwork:FC = () => {
|
|||||||
<div className="rb:mt-4" key={key}>
|
<div className="rb:mt-4" key={key}>
|
||||||
{t(`userMemory.ExtractedEntity_${key}`)}
|
{t(`userMemory.ExtractedEntity_${key}`)}
|
||||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||||
{entityProps[key]}
|
{Array.isArray(entityProps[key]) && entityProps[key].length > 0
|
||||||
|
? entityProps[key].map((vo, index) => <div key={index}>- {vo}</div>)
|
||||||
|
: entityProps[key]
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface Suggestions {
|
|||||||
const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
|
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,10 +32,14 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =>
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setLoading(true)
|
||||||
getEmotionSuggestions(id)
|
getEmotionSuggestions(id)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setSuggestions(res as Suggestions)
|
setSuggestions(res as Suggestions)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -63,7 +68,7 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =>
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
: <Empty size={88} className="rb:h-full" />
|
: <Empty size={88} subTitle={t(loading ? 'statementDetail.suggestionLoading' : 'empty.tableEmpty')} className="rb:h-full" />
|
||||||
}
|
}
|
||||||
</RbCard>
|
</RbCard>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,14 +20,22 @@ const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref)
|
|||||||
const habitsRef = useRef<{ handleRefresh: () => void; }>(null)
|
const habitsRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
if (!id) return
|
if (!id) {
|
||||||
generateProfile(id)
|
return Promise.resolve()
|
||||||
.then(() => {
|
}
|
||||||
preferencesRef.current?.handleRefresh()
|
return new Promise((resolve, reject) => {
|
||||||
portraitRef.current?.handleRefresh()
|
generateProfile(id)
|
||||||
interestAreasRef.current?.handleRefresh()
|
.then(() => {
|
||||||
habitsRef.current?.handleRefresh()
|
preferencesRef.current?.handleRefresh()
|
||||||
})
|
portraitRef.current?.handleRefresh()
|
||||||
|
interestAreasRef.current?.handleRefresh()
|
||||||
|
habitsRef.current?.handleRefresh()
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleRefresh
|
handleRefresh
|
||||||
|
|||||||
@@ -13,11 +13,20 @@ const StatementDetail = forwardRef((_props, ref) => {
|
|||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const suggestionsRef = useRef<{ handleRefresh: () => void; }>(null)
|
const suggestionsRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
if (!id) return
|
if (!id) {
|
||||||
generateSuggestions(id)
|
return Promise.resolve()
|
||||||
.then(() => {
|
}
|
||||||
suggestionsRef.current?.handleRefresh()
|
|
||||||
})
|
return new Promise((resolve, reject) => {
|
||||||
|
generateSuggestions(id)
|
||||||
|
.then(() => {
|
||||||
|
suggestionsRef.current?.handleRefresh()
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleRefresh
|
handleRefresh
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type FC, useEffect, useState, useMemo } 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, Select, Form, Space, Skeleton, Input, Button, Divider } from 'antd'
|
import { Row, Col, Skeleton, Button, Divider, Tooltip } from 'antd'
|
||||||
import RbCard from '@/components/RbCard/Card'
|
import RbCard from '@/components/RbCard/Card'
|
||||||
import {
|
import {
|
||||||
getConversations,
|
getConversations,
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
getConversationDetail,
|
getConversationDetail,
|
||||||
} from '@/api/memory'
|
} from '@/api/memory'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
import Tag from '@/components/Tag'
|
|
||||||
import RbAlert from '@/components/RbAlert'
|
import RbAlert from '@/components/RbAlert'
|
||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import ChatContent from '@/components/Chat/ChatContent'
|
import ChatContent from '@/components/Chat/ChatContent'
|
||||||
@@ -33,7 +32,6 @@ interface Detail {
|
|||||||
const WorkingDetail: FC = () => {
|
const WorkingDetail: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const [form] = Form.useForm()
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [data, setData] = useState<Conversation[]>([])
|
const [data, setData] = useState<Conversation[]>([])
|
||||||
const [messagesLoading, setMessagesLoading] = useState<boolean>(false)
|
const [messagesLoading, setMessagesLoading] = useState<boolean>(false)
|
||||||
@@ -110,13 +108,15 @@ const WorkingDetail: FC = () => {
|
|||||||
<div className="rb:h-full! rb:border-r rb:border-[#EAECEE] rb:py-3 rb:px-4">
|
<div className="rb:h-full! rb:border-r rb:border-[#EAECEE] rb:py-3 rb:px-4">
|
||||||
{data.map(item => (
|
{data.map(item => (
|
||||||
<div key={item.id} className="rb:mb-3">
|
<div key={item.id} className="rb:mb-3">
|
||||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
<Tooltip title={item.title}>
|
||||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
|
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap", {
|
||||||
})}
|
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
|
||||||
onClick={() => setSelected(item)}
|
})}
|
||||||
>
|
onClick={() => setSelected(item)}
|
||||||
{item.title}
|
>
|
||||||
</div>
|
{item.title}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type FC, useEffect, useState, useMemo, useRef } from 'react'
|
|||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Dropdown, Button } from 'antd'
|
import { Dropdown, Button } from 'antd'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import PageHeader from '../components/PageHeader'
|
import PageHeader from '../components/PageHeader'
|
||||||
import StatementDetail from './StatementDetail'
|
import StatementDetail from './StatementDetail'
|
||||||
@@ -46,18 +47,30 @@ const Detail: FC = () => {
|
|||||||
const onClick = ({ key }: { key: string }) => {
|
const onClick = ({ key }: { key: string }) => {
|
||||||
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
|
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
|
setLoading(true)
|
||||||
|
let response: any = null
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'FORGET_MEMORY':
|
case 'FORGET_MEMORY':
|
||||||
forgetDetailRef.current?.handleRefresh()
|
forgetDetailRef.current?.handleRefresh()
|
||||||
break;
|
break;
|
||||||
case 'EMOTIONAL_MEMORY':
|
case 'EMOTIONAL_MEMORY':
|
||||||
statementDetailRef.current?.handleRefresh()
|
response = statementDetailRef.current?.handleRefresh()
|
||||||
break
|
break
|
||||||
case 'IMPLICIT_MEMORY':
|
case 'IMPLICIT_MEMORY':
|
||||||
implicitDetailRef.current?.handleRefresh()
|
response = implicitDetailRef.current?.handleRefresh()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response instanceof Promise) {
|
||||||
|
response.finally(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'GRAPH') {
|
if (type === 'GRAPH') {
|
||||||
@@ -80,8 +93,8 @@ const Detail: FC = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
}
|
}
|
||||||
extra={['FORGET_MEMORY', 'EMOTIONAL_MEMORY', 'IMPLICIT_MEMORY'].includes(type as string) &&
|
extra={['FORGET_MEMORY', 'EMOTIONAL_MEMORY', 'IMPLICIT_MEMORY'].includes(type as string) &&
|
||||||
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}>
|
<Button type="primary" ghost size="small" className="rb:h-6! rb:px-2! rb:leading-5.5!" loading={loading} onClick={handleRefresh}>
|
||||||
<img src={refreshIcon} className="rb:w-4 rb:h-4" />
|
{!loading && <img src={refreshIcon} className="rb:w-4 rb:h-4" /> }
|
||||||
{t('common.refresh')}
|
{t('common.refresh')}
|
||||||
</Button>}
|
</Button>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { TextNode, $createTextNode } from 'lexical';
|
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
||||||
|
|
||||||
const Jinja2HighlightPlugin = () => {
|
const Jinja2HighlightPlugin = () => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
@@ -18,6 +18,16 @@ const Jinja2HighlightPlugin = () => {
|
|||||||
const parent = textNode.getParent();
|
const parent = textNode.getParent();
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
|
|
||||||
|
// Preserve selection
|
||||||
|
const selection = $getSelection();
|
||||||
|
let selectionOffset = null;
|
||||||
|
if ($isRangeSelection(selection)) {
|
||||||
|
const anchor = selection.anchor;
|
||||||
|
if (anchor.getNode() === textNode) {
|
||||||
|
selectionOffset = anchor.offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tokens = tokenizeJinja2(text);
|
const tokens = tokenizeJinja2(text);
|
||||||
|
|
||||||
// Skip if no meaningful tokenization (only one text token)
|
// Skip if no meaningful tokenization (only one text token)
|
||||||
@@ -85,6 +95,19 @@ const Jinja2HighlightPlugin = () => {
|
|||||||
for (let i = 1; i < newNodes.length; i++) {
|
for (let i = 1; i < newNodes.length; i++) {
|
||||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore selection
|
||||||
|
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
||||||
|
let currentOffset = 0;
|
||||||
|
for (const node of newNodes) {
|
||||||
|
const nodeLength = node.getTextContent().length;
|
||||||
|
if (currentOffset + nodeLength >= selectionOffset) {
|
||||||
|
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentOffset += nodeLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectPr
|
|||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
import VariableSelect from '../VariableSelect'
|
import VariableSelect from '../VariableSelect'
|
||||||
import Editor from '../../Editor'
|
import Editor from '../../Editor'
|
||||||
import { edgeAttrs } from '../../../constant'
|
import { edgeAttrs, portArgs } from '../../../constant'
|
||||||
|
|
||||||
interface CaseListProps {
|
interface CaseListProps {
|
||||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
||||||
@@ -92,6 +92,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
selectedNode.addPort({
|
selectedNode.addPort({
|
||||||
id: 'CASE1',
|
id: 'CASE1',
|
||||||
group: 'right',
|
group: 'right',
|
||||||
|
args: portArgs,
|
||||||
attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }}
|
attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
selectedNode.addPort({
|
selectedNode.addPort({
|
||||||
id: `CASE${i + 1}`,
|
id: `CASE${i + 1}`,
|
||||||
group: 'right',
|
group: 'right',
|
||||||
|
args: portArgs,
|
||||||
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
|
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -108,6 +110,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
selectedNode.addPort({
|
selectedNode.addPort({
|
||||||
id: `CASE${caseCount + 1}`,
|
id: `CASE${caseCount + 1}`,
|
||||||
group: 'right',
|
group: 'right',
|
||||||
|
args: portArgs,
|
||||||
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
|
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Graph, Node } from '@antv/x6';
|
|||||||
|
|
||||||
import Editor from '../../Editor';
|
import Editor from '../../Editor';
|
||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
import { edgeAttrs } from '../../../constant'
|
import { edgeAttrs, portArgs } from '../../../constant'
|
||||||
|
|
||||||
interface CategoryListProps {
|
interface CategoryListProps {
|
||||||
parentName: string;
|
parentName: string;
|
||||||
@@ -55,7 +55,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
|||||||
selectedNode.addPort({
|
selectedNode.addPort({
|
||||||
id: `CASE${i + 1}`,
|
id: `CASE${i + 1}`,
|
||||||
group: 'right',
|
group: 'right',
|
||||||
args: i === 0 ? { dy: 24 } : undefined,
|
args: portArgs,
|
||||||
attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } }
|
attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,11 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
|||||||
const form = Form.useFormInstance()
|
const form = Form.useFormInstance()
|
||||||
const values = Form.useWatch([], form) || {}
|
const values = Form.useWatch([], form) || {}
|
||||||
|
|
||||||
console.log('JinjaRender values', values)
|
|
||||||
|
|
||||||
const prevMappingNamesRef = useRef<string[]>([])
|
const prevMappingNamesRef = useRef<string[]>([])
|
||||||
const prevTemplateVarsRef = useRef<string[]>([])
|
const prevTemplateVarsRef = useRef<string[]>([])
|
||||||
const syncTimeoutRef = useRef<number | null>(null)
|
|
||||||
const isSyncingRef = useRef(false)
|
const isSyncingRef = useRef(false)
|
||||||
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
|
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
|
||||||
|
const editorKeyRef = useRef(0)
|
||||||
|
|
||||||
// Reset refs when node changes
|
// Reset refs when node changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,46 +66,39 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
|||||||
|
|
||||||
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
|
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
|
||||||
|
|
||||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
|
let updatedTemplate = String(form.getFieldValue('template') || '')
|
||||||
const activeElement = document.activeElement as HTMLElement
|
|
||||||
|
|
||||||
syncTimeoutRef.current = setTimeout(() => {
|
prevNames.forEach((oldName, index) => {
|
||||||
let updatedTemplate = String(form.getFieldValue('template') || '')
|
const newName = currentMappingNames[index]
|
||||||
|
if (newName && oldName !== newName) {
|
||||||
prevNames.forEach((oldName, index) => {
|
updatedTemplate = updatedTemplate.replace(
|
||||||
const newName = currentMappingNames[index]
|
new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'),
|
||||||
if (newName && oldName !== newName) {
|
`{{${newName}}}`
|
||||||
updatedTemplate = updatedTemplate.replace(
|
)
|
||||||
new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'),
|
|
||||||
`{{${newName}}}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (updatedTemplate !== form.getFieldValue('template')) {
|
|
||||||
isSyncingRef.current = true
|
|
||||||
lastSyncSourceRef.current = 'mapping'
|
|
||||||
|
|
||||||
prevTemplateVarsRef.current = extractTemplateVars(updatedTemplate)
|
|
||||||
prevMappingNamesRef.current = currentMappingNames
|
|
||||||
form.setFieldValue('template', updatedTemplate)
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
activeElement?.focus?.()
|
|
||||||
setTimeout(() => {
|
|
||||||
isSyncingRef.current = false
|
|
||||||
lastSyncSourceRef.current = null
|
|
||||||
}, 50)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
prevMappingNamesRef.current = currentMappingNames
|
|
||||||
}
|
}
|
||||||
}, 0)
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if (updatedTemplate !== form.getFieldValue('template')) {
|
||||||
|
isSyncingRef.current = true
|
||||||
|
lastSyncSourceRef.current = 'mapping'
|
||||||
|
|
||||||
|
prevTemplateVarsRef.current = extractTemplateVars(updatedTemplate)
|
||||||
|
prevMappingNamesRef.current = currentMappingNames
|
||||||
|
form.setFieldValue('template', updatedTemplate)
|
||||||
|
editorKeyRef.current++
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isSyncingRef.current = false
|
||||||
|
lastSyncSourceRef.current = null
|
||||||
|
}, 0)
|
||||||
|
} else {
|
||||||
|
prevMappingNamesRef.current = currentMappingNames
|
||||||
|
}
|
||||||
}, [values?.mapping, selectedNode?.data?.type, form])
|
}, [values?.mapping, selectedNode?.data?.type, form])
|
||||||
|
|
||||||
// Sync mapping when template variables change
|
// Sync mapping when template variables change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('values?.template', values?.template)
|
|
||||||
if (
|
if (
|
||||||
isSyncingRef.current ||
|
isSyncingRef.current ||
|
||||||
lastSyncSourceRef.current === 'template' ||
|
lastSyncSourceRef.current === 'template' ||
|
||||||
@@ -155,11 +146,10 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Remove unused mappings and duplicates
|
// Remove duplicates only
|
||||||
const seenNames = new Set<string>()
|
const seenNames = new Set<string>()
|
||||||
const finalMapping = updatedMapping.filter(item => {
|
const finalMapping = updatedMapping.filter(item => {
|
||||||
const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`)
|
if (!item.name || seenNames.has(item.name)) return false
|
||||||
if (!isUsed || !item.name || seenNames.has(item.name)) return false
|
|
||||||
seenNames.add(item.name)
|
seenNames.add(item.name)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -190,6 +180,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
|||||||
|
|
||||||
<Form.Item name="template">
|
<Form.Item name="template">
|
||||||
<MessageEditor
|
<MessageEditor
|
||||||
|
key={editorKeyRef.current}
|
||||||
title={t('workflow.config.jinja-render.template')}
|
title={t('workflow.config.jinja-render.template')}
|
||||||
isArray={false}
|
isArray={false}
|
||||||
parentName="template"
|
parentName="template"
|
||||||
|
|||||||
@@ -190,6 +190,12 @@ export const useVariableList = (
|
|||||||
if (iv?.dataType.startsWith('array[')) itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1');
|
if (iv?.dataType.startsWith('array[')) itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||||
addVariable(list, keys, `${pid}_item`, 'item', itemType, `${pid}.item`, pd);
|
addVariable(list, keys, `${pid}_item`, 'item', itemType, `${pid}.item`, pd);
|
||||||
addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd);
|
addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd);
|
||||||
|
} else if (pd.type === 'iteration' && !pd.config.input.defaultValue) {
|
||||||
|
let itemType = 'object';
|
||||||
|
const iv = list.find(v => `{{${v.value}}}` === pd.config.input.defaultValue);
|
||||||
|
if (iv?.dataType.startsWith('array[')) itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||||
|
addVariable(list, keys, `${pid}_item`, 'item', 'string', `${pid}.item`, pd);
|
||||||
|
addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,7 +291,69 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
return filteredList;
|
return filteredList;
|
||||||
}
|
}
|
||||||
if (nodeType === 'iteration' && key === 'output') {
|
if (nodeType === 'iteration' && key === 'output') {
|
||||||
return variableList.filter(variable => variable.value.includes('sys.'));
|
let filteredList = variableList.filter(variable => variable.value.includes('sys.'));
|
||||||
|
// Add child node output variables for loop nodes
|
||||||
|
if (selectedNode) {
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (graph) {
|
||||||
|
const nodes = graph.getNodes();
|
||||||
|
const childNodes = nodes.filter(node => {
|
||||||
|
const nodeData = node.getData();
|
||||||
|
return nodeData?.cycle === selectedNode.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add output variables from child nodes
|
||||||
|
childNodes.forEach(childNode => {
|
||||||
|
const childData = childNode.getData();
|
||||||
|
const childNodeId = childData.id;
|
||||||
|
|
||||||
|
// Add child node output variables based on their type
|
||||||
|
switch (childData.type) {
|
||||||
|
case 'llm':
|
||||||
|
case 'jinja-render':
|
||||||
|
case 'tool':
|
||||||
|
const outputKey = `${childNodeId}_output`;
|
||||||
|
const existingOutput = filteredList.find(v => v.key === outputKey);
|
||||||
|
if (!existingOutput) {
|
||||||
|
filteredList.push({
|
||||||
|
key: outputKey,
|
||||||
|
label: 'output',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${childNodeId}.output`,
|
||||||
|
nodeData: childData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'http-request':
|
||||||
|
const bodyKey = `${childNodeId}_body`;
|
||||||
|
const statusKey = `${childNodeId}_status_code`;
|
||||||
|
if (!filteredList.find(v => v.key === bodyKey)) {
|
||||||
|
filteredList.push({
|
||||||
|
key: bodyKey,
|
||||||
|
label: 'body',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${childNodeId}.body`,
|
||||||
|
nodeData: childData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!filteredList.find(v => v.key === statusKey)) {
|
||||||
|
filteredList.push({
|
||||||
|
key: statusKey,
|
||||||
|
label: 'status_code',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'number',
|
||||||
|
value: `${childNodeId}.status_code`,
|
||||||
|
nodeData: childData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredList;
|
||||||
}
|
}
|
||||||
if (nodeType === 'iteration') {
|
if (nodeType === 'iteration') {
|
||||||
return variableList.filter(variable => variable.dataType.includes('array'));
|
return variableList.filter(variable => variable.dataType.includes('array'));
|
||||||
@@ -411,7 +473,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
/>
|
/>
|
||||||
: selectedNode?.data?.type === 'tool'
|
: selectedNode?.data?.type === 'tool'
|
||||||
? <ToolConfig options={variableList} />
|
? <ToolConfig options={variableList} />
|
||||||
: selectedNode?.data.type === 'jinja-render'
|
: selectedNode?.data?.type === 'jinja-render'
|
||||||
? <JinjaRender
|
? <JinjaRender
|
||||||
selectedNode={selectedNode}
|
selectedNode={selectedNode}
|
||||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||||
|
|||||||
@@ -564,6 +564,7 @@ const defaultPortItems = [
|
|||||||
// { group: 'bottom' },
|
// { group: 'bottom' },
|
||||||
{ group: 'left' }
|
{ group: 'left' }
|
||||||
];
|
];
|
||||||
|
export const portArgs = { dy: 18 }
|
||||||
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||||
iteration: {
|
iteration: {
|
||||||
width: 240,
|
width: 240,
|
||||||
@@ -591,8 +592,8 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
|||||||
groups: defaultPortGroups,
|
groups: defaultPortGroups,
|
||||||
items: [
|
items: [
|
||||||
{ group: 'left' },
|
{ group: 'left' },
|
||||||
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: 'IF', fontSize: 12, color: '#5B6167' }} },
|
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, color: '#5B6167' }} },
|
||||||
{ group: 'right', id: 'CASE2', attrs: { text: { text: 'ELSE', fontSize: 12, color: '#5B6167' }} }
|
{ group: 'right', id: 'CASE2', args: portArgs, attrs: { text: { text: 'ELSE', fontSize: 12, color: '#5B6167' }} }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -604,8 +605,8 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
|||||||
groups: defaultPortGroups,
|
groups: defaultPortGroups,
|
||||||
items: [
|
items: [
|
||||||
{ group: 'left' },
|
{ group: 'left' },
|
||||||
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } },
|
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } },
|
||||||
{ group: 'right', id: 'CASE2', attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } }
|
{ group: 'right', id: 'CASE2', args: portArgs, attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { App } from 'antd'
|
|||||||
import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '@antv/x6';
|
import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '@antv/x6';
|
||||||
import { register } from '@antv/x6-react-shape';
|
import { register } from '@antv/x6-react-shape';
|
||||||
|
|
||||||
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color } from '../constant';
|
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portArgs } from '../constant';
|
||||||
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
|
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
|
||||||
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
||||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||||
@@ -132,7 +132,7 @@ export const useWorkflowGraph = ({
|
|||||||
|
|
||||||
const portItems: PortMetadata[] = [
|
const portItems: PortMetadata[] = [
|
||||||
{ group: 'left' },
|
{ group: 'left' },
|
||||||
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} }
|
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 添加 ELIF 端口
|
// 添加 ELIF 端口
|
||||||
@@ -140,6 +140,7 @@ export const useWorkflowGraph = ({
|
|||||||
portItems.push({
|
portItems.push({
|
||||||
group: 'right',
|
group: 'right',
|
||||||
id: `CASE${i + 1}`,
|
id: `CASE${i + 1}`,
|
||||||
|
args: portArgs,
|
||||||
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
|
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -148,6 +149,7 @@ export const useWorkflowGraph = ({
|
|||||||
portItems.push({
|
portItems.push({
|
||||||
group: 'right',
|
group: 'right',
|
||||||
id: `CASE${caseCount + 1}`,
|
id: `CASE${caseCount + 1}`,
|
||||||
|
args: portArgs,
|
||||||
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
|
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,12 +175,12 @@ export const useWorkflowGraph = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 添加分类端口
|
// 添加分类端口
|
||||||
config.categories.forEach((category: any, index: number) => {
|
config.categories.forEach((_category: any, index: number) => {
|
||||||
portItems.push({
|
portItems.push({
|
||||||
group: 'right',
|
group: 'right',
|
||||||
id: `CASE${index + 1}`,
|
id: `CASE${index + 1}`,
|
||||||
args: index === 0 ? { dy: 24 } : undefined,
|
args: portArgs,
|
||||||
attrs: { text: { text: category.class_name || `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }}
|
attrs: { text: { text: `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user