Feat: add agent share team viewer (#6222)

### What problem does this PR solve?
Allow member view agent  
#  Canvas editor

![image](https://github.com/user-attachments/assets/042af36d-5fd1-43e2-acf7-05869220a1c1)
# List agent

![image](https://github.com/user-attachments/assets/8b9c7376-780b-47ff-8f5c-6c0e7358158d)
# Setting 

![image](https://github.com/user-attachments/assets/6cb7d12a-7a66-4dd7-9acc-5b53ff79a10a)
 
_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._

### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
so95 2025-03-19 18:04:13 +07:00 committed by GitHub
parent d17ec26c56
commit 344727f9ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1665 additions and 1164 deletions

View File

@ -18,6 +18,7 @@ import traceback
from flask import request, Response from flask import request, Response
from flask_login import login_required, current_user from flask_login import login_required, current_user
from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService
from api.db.services.user_service import TenantService
from api.db.services.user_canvas_version import UserCanvasVersionService from api.db.services.user_canvas_version import UserCanvasVersionService
from api.settings import RetCode from api.settings import RetCode
from api.utils import get_uuid from api.utils import get_uuid
@ -25,6 +26,7 @@ from api.utils.api_utils import get_json_result, server_error_response, validate
from agent.canvas import Canvas from agent.canvas import Canvas
from peewee import MySQLDatabase, PostgresqlDatabase from peewee import MySQLDatabase, PostgresqlDatabase
from api.db.db_models import APIToken from api.db.db_models import APIToken
import logging
import time import time
@manager.route('/templates', methods=['GET']) # noqa: F821 @manager.route('/templates', methods=['GET']) # noqa: F821
@ -86,10 +88,11 @@ def save():
@manager.route('/get/<canvas_id>', methods=['GET']) # noqa: F821 @manager.route('/get/<canvas_id>', methods=['GET']) # noqa: F821
@login_required @login_required
def get(canvas_id): def get(canvas_id):
e, c = UserCanvasService.get_by_id(canvas_id) e, c = UserCanvasService.get_by_tenant_id(canvas_id)
logging.info(f"get canvas_id: {canvas_id} c: {c}")
if not e: if not e:
return get_data_error_result(message="canvas not found.") return get_data_error_result(message="canvas not found.")
return get_json_result(data=c.to_dict()) return get_json_result(data=c)
@manager.route('/getsse/<canvas_id>', methods=['GET']) # type: ignore # noqa: F821 @manager.route('/getsse/<canvas_id>', methods=['GET']) # type: ignore # noqa: F821
def getsse(canvas_id): def getsse(canvas_id):
@ -288,10 +291,6 @@ def test_db_connect():
return get_json_result(data="Database Connection Successful!") return get_json_result(data="Database Connection Successful!")
except Exception as e: except Exception as e:
return server_error_response(e) return server_error_response(e)
#api get list version dsl of canvas #api get list version dsl of canvas
@manager.route('/getlistversion/<canvas_id>', methods=['GET']) # noqa: F821 @manager.route('/getlistversion/<canvas_id>', methods=['GET']) # noqa: F821
@login_required @login_required
@ -301,7 +300,6 @@ def getlistversion(canvas_id):
return get_json_result(data=list) return get_json_result(data=list)
except Exception as e: except Exception as e:
return get_data_error_result(message=f"Error getting history files: {e}") return get_data_error_result(message=f"Error getting history files: {e}")
#api get version dsl of canvas #api get version dsl of canvas
@manager.route('/getversion/<version_id>', methods=['GET']) # noqa: F821 @manager.route('/getversion/<version_id>', methods=['GET']) # noqa: F821
@login_required @login_required
@ -313,3 +311,42 @@ def getversion( version_id):
return get_json_result(data=version.to_dict()) return get_json_result(data=version.to_dict())
except Exception as e: except Exception as e:
return get_json_result(data=f"Error getting history file: {e}") return get_json_result(data=f"Error getting history file: {e}")
@manager.route('/listteam', methods=['GET']) # noqa: F821
@login_required
def list_kbs():
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 1))
items_per_page = int(request.args.get("page_size", 150))
orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True)
try:
tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
kbs, total = UserCanvasService.get_by_tenant_ids(
[m["tenant_id"] for m in tenants], current_user.id, page_number,
items_per_page, orderby, desc, keywords)
return get_json_result(data={"kbs": kbs, "total": total})
except Exception as e:
return server_error_response(e)
@manager.route('/setting', methods=['POST']) # noqa: F821
@validate_request("id", "title", "permission")
@login_required
def setting():
req = request.json
req["user_id"] = current_user.id
e,flow = UserCanvasService.get_by_id(req["id"])
if not e:
return get_data_error_result(message="canvas not found.")
flow = flow.to_dict()
flow["title"] = req["title"]
if req["description"]:
flow["description"] = req["description"]
if req["permission"]:
flow["permission"] = req["permission"]
if req["avatar"]:
flow["avatar"] = req["avatar"]
if not UserCanvasService.query(user_id=current_user.id, id=req["id"]):
return get_json_result(
data=False, message='Only owner of canvas authorized for this operation.',
code=RetCode.OPERATING_ERROR)
num= UserCanvasService.update_by_id(req["id"], flow)
return get_json_result(data=num)

View File

@ -968,6 +968,12 @@ class UserCanvas(DataBaseModel):
user_id = CharField(max_length=255, null=False, help_text="user_id", index=True) user_id = CharField(max_length=255, null=False, help_text="user_id", index=True)
title = CharField(max_length=255, null=True, help_text="Canvas title") title = CharField(max_length=255, null=True, help_text="Canvas title")
permission = CharField(
max_length=16,
null=False,
help_text="me|team",
default="me",
index=True)
description = TextField(null=True, help_text="Canvas description") description = TextField(null=True, help_text="Canvas description")
canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True) canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True)
dsl = JSONField(null=True, default={}) dsl = JSONField(null=True, default={})
@ -1140,3 +1146,11 @@ def migrate_db():
) )
except Exception: except Exception:
pass pass
try:
migrate(
migrator.add_column("user_canvas", "permission",
CharField(max_length=16, null=False, help_text="me|team", default="me", index=True))
)
except Exception:
pass

View File

@ -18,12 +18,13 @@ import time
import traceback import traceback
from uuid import uuid4 from uuid import uuid4
from agent.canvas import Canvas from agent.canvas import Canvas
from api.db.db_models import DB, CanvasTemplate, UserCanvas, API4Conversation from api.db import TenantPermission
from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation
from api.db.services.api_service import API4ConversationService from api.db.services.api_service import API4ConversationService
from api.db.services.common_service import CommonService from api.db.services.common_service import CommonService
from api.db.services.conversation_service import structure_answer from api.db.services.conversation_service import structure_answer
from api.utils import get_uuid from api.utils import get_uuid
from peewee import fn
class CanvasTemplateService(CommonService): class CanvasTemplateService(CommonService):
model = CanvasTemplate model = CanvasTemplate
@ -51,6 +52,73 @@ class UserCanvasService(CommonService):
return list(agents.dicts()) return list(agents.dicts())
@classmethod
@DB.connection_context()
def get_by_tenant_id(cls, pid):
try:
fields = [
cls.model.id,
cls.model.avatar,
cls.model.title,
cls.model.dsl,
cls.model.description,
cls.model.permission,
cls.model.update_time,
cls.model.user_id,
cls.model.create_time,
cls.model.create_date,
cls.model.update_date,
User.nickname,
User.avatar.alias('tenant_avatar'),
]
angents = cls.model.select(*fields) \
.join(User, on=(cls.model.user_id == User.id)) \
.where(cls.model.id == pid)
# obj = cls.model.query(id=pid)[0]
return True, angents.dicts()[0]
except Exception as e:
print(e)
return False, None
@classmethod
@DB.connection_context()
def get_by_tenant_ids(cls, joined_tenant_ids, user_id,
page_number, items_per_page,
orderby, desc, keywords,
):
fields = [
cls.model.id,
cls.model.avatar,
cls.model.title,
cls.model.dsl,
cls.model.description,
cls.model.permission,
User.nickname,
User.avatar.alias('tenant_avatar'),
cls.model.update_time
]
if keywords:
angents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
((cls.model.user_id.in_(joined_tenant_ids) & (cls.model.permission ==
TenantPermission.TEAM.value)) | (
cls.model.user_id == user_id)),
(fn.LOWER(cls.model.title).contains(keywords.lower()))
)
else:
angents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
((cls.model.user_id.in_(joined_tenant_ids) & (cls.model.permission ==
TenantPermission.TEAM.value)) | (
cls.model.user_id == user_id))
)
if desc:
angents = angents.order_by(cls.model.getter_by(orderby).desc())
else:
angents = angents.order_by(cls.model.getter_by(orderby).asc())
count = angents.count()
angents = angents.paginate(page_number, items_per_page)
return list(angents.dicts()), count
def completion(tenant_id, agent_id, question, session_id=None, stream=True, **kwargs): def completion(tenant_id, agent_id, question, session_id=None, stream=True, **kwargs):
e, cvs = UserCanvasService.get_by_id(agent_id) e, cvs = UserCanvasService.get_by_id(agent_id)

View File

@ -171,6 +171,27 @@ export const useFetchFlow = (): {
return { data, loading, refetch }; return { data, loading, refetch };
}; };
export const useSettingFlow = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['SettingFlow'],
mutationFn: async (params: any) => {
const ret = await flowService.settingCanvas(params);
if (ret?.data?.code === 0) {
message.success('success');
} else {
message.error(ret?.data?.data);
}
return ret;
},
});
return { data, loading, settingFlow: mutateAsync };
};
export const useFetchFlowSSE = (): { export const useFetchFlowSSE = (): {
data: IFlow; data: IFlow;
loading: boolean; loading: boolean;
@ -244,7 +265,9 @@ export const useDeleteFlow = () => {
mutationFn: async (canvasIds: string[]) => { mutationFn: async (canvasIds: string[]) => {
const { data } = await flowService.removeCanvas({ canvasIds }); const { data } = await flowService.removeCanvas({ canvasIds });
if (data.code === 0) { if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] }); queryClient.invalidateQueries({
queryKey: ['infiniteFetchFlowListTeam'],
});
} }
return data?.data ?? []; return data?.data ?? [];
}, },

View File

@ -37,6 +37,8 @@ export declare interface IFlow {
update_date: string; update_date: string;
update_time: number; update_time: number;
user_id: string; user_id: string;
permission: string;
nickname: string;
} }
export interface IFlowTemplate { export interface IFlowTemplate {

File diff suppressed because it is too large Load Diff

View File

@ -1192,6 +1192,15 @@ This delimiter is used to split the input text into several text pieces echo of
addCategory: 'Add category', addCategory: 'Add category',
categoryName: 'Category name', categoryName: 'Category name',
nextStep: 'Next step', nextStep: 'Next step',
variableExtractDescription:
'Extract user information into global variable throughout the conversation',
variableExtract: 'Variables',
variables: 'Variables',
variablesTip: `Set the clear json key variable with a value of empty. e.g.
{
"UserCode":"",
"NumberPhone":""
}`,
datatype: 'MINE type of the HTTP request', datatype: 'MINE type of the HTTP request',
insertVariableTip: `Enter / Insert variables`, insertVariableTip: `Enter / Insert variables`,
historyversion: 'History version', historyversion: 'History version',
@ -1204,14 +1213,25 @@ This delimiter is used to split the input text into several text pieces echo of
version: 'Version', version: 'Version',
select: 'No version selected', select: 'No version selected',
}, },
}, setting: 'Setting',
footer: { settings: {
profile: 'All rights reserved @ React', upload: 'Upload',
}, photo: 'Photo',
layout: { permissions: 'Permission',
file: 'file', permissionsTip: 'You can set the permissions of the team members here.',
knowledge: 'knowledge', me: 'me',
chat: 'chat', team: 'Team',
},
noMoreData: 'No more data',
searchAgentPlaceholder: 'Search agent',
footer: {
profile: 'All rights reserved @ React',
},
layout: {
file: 'file',
knowledge: 'knowledge',
chat: 'chat',
},
}, },
}, },
}; };

View File

@ -722,7 +722,8 @@ export default {
s3: 'S3 上傳', s3: 'S3 上傳',
preview: '預覽', preview: '預覽',
fileError: '文件錯誤', fileError: '文件錯誤',
uploadLimit: '本地部署的單次上傳檔案總大小上限為 1GB單次批量上傳檔案數不超過 32單個帳戶不限檔案數量。', uploadLimit:
'本地部署的單次上傳檔案總大小上限為 1GB單次批量上傳檔案數不超過 32單個帳戶不限檔案數量。',
destinationFolder: '目標資料夾', destinationFolder: '目標資料夾',
}, },
flow: { flow: {

View File

@ -0,0 +1,121 @@
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow, useSettingFlow } from '@/hooks/flow-hooks';
import { normFile } from '@/utils/file-util';
import { PlusOutlined } from '@ant-design/icons';
import { Form, Input, Modal, Radio, Upload } from 'antd';
import React, { useCallback, useEffect } from 'react';
export function useFlowSettingModal() {
const [visibleSettingModal, setVisibleSettingMModal] = React.useState(false);
return {
visibleSettingModal,
setVisibleSettingMModal,
};
}
type FlowSettingModalProps = {
visible: boolean;
hideModal: () => void;
id: string;
};
export const FlowSettingModal = ({
hideModal,
visible,
id,
}: FlowSettingModalProps) => {
const { data, refetch } = useFetchFlow();
const [form] = Form.useForm();
const { t } = useTranslate('flow.settings');
const { loading, settingFlow } = useSettingFlow();
// Initialize form with data when it becomes available
useEffect(() => {
if (data) {
form.setFieldsValue({
title: data.title,
description: data.description,
permission: data.permission,
avatar: data.avatar ? [{ thumbUrl: data.avatar }] : [],
});
}
}, [data, form]);
const handleSubmit = useCallback(async () => {
if (!id) return;
try {
const { avatar, ...others } = await form.validateFields();
const param = {
...others,
id,
avatar: avatar && avatar.length > 0 ? avatar[0].thumbUrl : '',
};
settingFlow(param);
} catch (error) {
console.error('Validation failed:', error);
}
}, [form, id, settingFlow]);
React.useEffect(() => {
if (!loading && refetch && visible) {
refetch();
}
}, [loading, refetch, visible]);
return (
<Modal
confirmLoading={loading}
title={'Agent Setting'}
open={visible}
onCancel={hideModal}
onOk={handleSubmit}
okText={t('save', { keyPrefix: 'common' })}
cancelText={t('cancel', { keyPrefix: 'common' })}
>
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
layout="horizontal"
style={{ maxWidth: 600 }}
>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: 'Please input a title!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="avatar"
label={t('photo')}
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
beforeUpload={() => false}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>{t('upload')}</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label="Description">
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item
name="permission"
label={t('permissions')}
tooltip={t('permissionsTip')}
rules={[{ required: true }]}
>
<Radio.Group>
<Radio value="me">{t('me')}</Radio>
<Radio value="team">{t('team')}</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -1,3 +1,10 @@
.flowHeader { .flowHeader {
padding: 10px 20px; padding: 10px 20px;
} }
.hideRibbon {
display: none !important;
}
.ribbon {
top: 4px;
}

View File

@ -3,10 +3,13 @@ import { useShowEmbedModal } from '@/components/api-service/hooks';
import { SharedFrom } from '@/constants/chat'; import { SharedFrom } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow } from '@/hooks/flow-hooks'; import { useFetchFlow } from '@/hooks/flow-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { ArrowLeftOutlined } from '@ant-design/icons'; import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd'; import { Badge, Button, Flex, Space } from 'antd';
import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Link, useParams } from 'umi'; import { Link, useParams } from 'umi';
import { FlowSettingModal, useFlowSettingModal } from '../flow-setting';
import { import {
useGetBeginNodeDataQuery, useGetBeginNodeDataQuery,
useGetBeginNodeDataQueryIsSafe, useGetBeginNodeDataQueryIsSafe,
@ -32,6 +35,8 @@ interface IProps {
const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const { saveGraph } = useSaveGraph(); const { saveGraph } = useSaveGraph();
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const { data: userInfo } = useFetchUserInfo();
const { data } = useFetchFlow(); const { data } = useFetchFlow();
const { t } = useTranslate('flow'); const { t } = useTranslate('flow');
const { id } = useParams(); const { id } = useParams();
@ -39,6 +44,8 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
const { showEmbedModal, hideEmbedModal, embedVisible, beta } = const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal(); useShowEmbedModal();
const { setVisibleSettingMModal, visibleSettingModal } =
useFlowSettingModal();
const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe(); const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe();
const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } = const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } =
useHistoryVersionModal(); useHistoryVersionModal();
@ -55,6 +62,10 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
} }
}, [getBeginNodeDataQuery, handleRun, showChatDrawer]); }, [getBeginNodeDataQuery, handleRun, showChatDrawer]);
const showSetting = useCallback(() => {
setVisibleSettingMModal(true);
}, [setVisibleSettingMModal]);
const showListVersion = useCallback(() => { const showListVersion = useCallback(() => {
setVisibleHistoryVersionModal(true); setVisibleHistoryVersionModal(true);
}, [setVisibleHistoryVersionModal]); }, [setVisibleHistoryVersionModal]);
@ -66,31 +77,56 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
gap={'large'} gap={'large'}
className={styles.flowHeader} className={styles.flowHeader}
> >
<Badge.Ribbon
text={data?.nickname}
style={{ marginRight: -data?.nickname?.length * 5 }}
color={userInfo?.nickname === data?.nickname ? '#1677ff' : 'pink'}
className={classNames(styles.ribbon, {
[styles.hideRibbon]: data.permission !== 'team',
})}
>
<Space className={styles.headerTitle} size={'large'}>
<Link to={`/flow`}>
<ArrowLeftOutlined />
</Link>
<div className="flex flex-col">
<span className="font-semibold text-[18px]">{data.title}</span>
<span className="font-normal text-sm">
{t('autosaved')} {time}
</span>
</div>
</Space>
</Badge.Ribbon>
<Space size={'large'}> <Space size={'large'}>
<Link to={`/flow`}> <Button
<ArrowLeftOutlined /> disabled={userInfo.nickname !== data.nickname}
</Link> onClick={handleRunAgent}
<div className="flex flex-col"> >
<span className="font-semibold text-[18px]">{data.title}</span>
<span className="font-normal text-sm">
{t('autosaved')} {time}
</span>
</div>
</Space>
<Space size={'large'}>
<Button onClick={handleRunAgent}>
<b>{t('run')}</b> <b>{t('run')}</b>
</Button> </Button>
<Button type="primary" onClick={() => saveGraph()}> <Button
disabled={userInfo.nickname !== data.nickname}
type="primary"
onClick={() => saveGraph()}
>
<b>{t('save')}</b> <b>{t('save')}</b>
</Button> </Button>
<Button <Button
type="primary" type="primary"
onClick={handleShowEmbedModal} onClick={handleShowEmbedModal}
disabled={!isBeginNodeDataQuerySafe} disabled={
!isBeginNodeDataQuerySafe || userInfo.nickname !== data.nickname
}
> >
<b>{t('embedIntoSite', { keyPrefix: 'common' })}</b> <b>{t('embedIntoSite', { keyPrefix: 'common' })}</b>
</Button> </Button>
<Button
disabled={userInfo.nickname !== data.nickname}
type="primary"
onClick={showSetting}
>
<b>{t('setting')}</b>
</Button>
<Button type="primary" onClick={showListVersion}> <Button type="primary" onClick={showListVersion}>
<b>{t('historyversion')}</b> <b>{t('historyversion')}</b>
</Button> </Button>
@ -106,6 +142,13 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
isAgent isAgent
></EmbedModal> ></EmbedModal>
)} )}
{visibleSettingModal && (
<FlowSettingModal
id={id || ''}
visible={visibleSettingModal}
hideModal={() => setVisibleSettingMModal(false)}
></FlowSettingModal>
)}
{visibleHistoryVersionModal && ( {visibleHistoryVersionModal && (
<HistoryVersionModal <HistoryVersionModal
id={id || ''} id={id || ''}

View File

@ -74,3 +74,11 @@
vertical-align: middle; vertical-align: middle;
} }
} }
.hideRibbon {
display: none !important;
}
.ribbon {
top: 4px;
}

View File

@ -1,22 +1,26 @@
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { CalendarOutlined } from '@ant-design/icons'; import { CalendarOutlined } from '@ant-design/icons';
import { Card, Typography } from 'antd'; import { Badge, Card, Typography } from 'antd';
import { useNavigate } from 'umi'; import { useNavigate } from 'umi';
import OperateDropdown from '@/components/operate-dropdown'; import OperateDropdown from '@/components/operate-dropdown';
import { useDeleteFlow } from '@/hooks/flow-hooks'; import { useDeleteFlow } from '@/hooks/flow-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { IFlow } from '@/interfaces/database/flow'; import { IFlow } from '@/interfaces/database/flow';
import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
import GraphAvatar from '../graph-avatar'; import GraphAvatar from '../graph-avatar';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
item: IFlow; item: IFlow;
onDelete?: (string: string) => void;
} }
const FlowCard = ({ item }: IProps) => { const FlowCard = ({ item }: IProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { deleteFlow } = useDeleteFlow(); const { deleteFlow } = useDeleteFlow();
const { data: userInfo } = useFetchUserInfo();
const removeFlow = useCallback(() => { const removeFlow = useCallback(() => {
return deleteFlow([item.id]); return deleteFlow([item.id]);
@ -27,33 +31,41 @@ const FlowCard = ({ item }: IProps) => {
}; };
return ( return (
<Card className={styles.card} onClick={handleCardClick}> <Badge.Ribbon
<div className={styles.container}> text={item?.nickname}
<div className={styles.content}> color={userInfo?.nickname === item?.nickname ? '#1677ff' : 'pink'}
<GraphAvatar avatar={item.avatar}></GraphAvatar> className={classNames(styles.ribbon, {
<OperateDropdown deleteItem={removeFlow}></OperateDropdown> [styles.hideRibbon]: item.permission !== 'team',
</div> })}
<div className={styles.titleWrapper}> >
<Typography.Title <Card className={styles.card} onClick={handleCardClick}>
className={styles.title} <div className={styles.container}>
ellipsis={{ tooltip: item.title }} <div className={styles.content}>
> <GraphAvatar avatar={item.avatar}></GraphAvatar>
{item.title} <OperateDropdown deleteItem={removeFlow}></OperateDropdown>
</Typography.Title> </div>
<p>{item.description}</p> <div className={styles.titleWrapper}>
</div> <Typography.Title
<div className={styles.footer}> className={styles.title}
<div className={styles.bottom}> ellipsis={{ tooltip: item.title }}
<div className={styles.bottomLeft}> >
<CalendarOutlined className={styles.leftIcon} /> {item.title}
<span className={styles.rightText}> </Typography.Title>
{formatDate(item.update_time)} <p>{item.description}</p>
</span> </div>
<div className={styles.footer}>
<div className={styles.bottom}>
<div className={styles.bottomLeft}>
<CalendarOutlined className={styles.leftIcon} />
<span className={styles.rightText}>
{formatDate(item.update_time)}
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </Card>
</Card> </Badge.Ribbon>
); );
}; };

View File

@ -1,16 +1,56 @@
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { import { useFetchFlowTemplates, useSetFlow } from '@/hooks/flow-hooks';
useFetchFlowList, import { useHandleSearchChange } from '@/hooks/logic-hooks';
useFetchFlowTemplates, import flowService from '@/services/flow-service';
useSetFlow, import { useInfiniteQuery } from '@tanstack/react-query';
} from '@/hooks/flow-hooks'; import { useDebounce } from 'ahooks';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'umi'; import { useNavigate } from 'umi';
export const useFetchDataOnMount = () => { export const useFetchDataOnMount = () => {
const { data, loading } = useFetchFlowList(); const { searchString, handleInputChange } = useHandleSearchChange();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
return { list: data, loading }; const PageSize = 30;
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['infiniteFetchFlowListTeam', debouncedSearchString],
queryFn: async ({ pageParam }) => {
const { data } = await flowService.listCanvasTeam({
page: pageParam,
page_size: PageSize,
keywords: debouncedSearchString,
});
const list = data?.data ?? [];
return list;
},
initialPageParam: 1,
getNextPageParam: (lastPage, pages, lastPageParam) => {
if (lastPageParam * PageSize <= lastPage.total) {
return lastPageParam + 1;
}
return undefined;
},
});
return {
data,
loading: isFetching,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
handleInputChange,
searchString,
};
}; };
export const useSaveFlow = () => { export const useSaveFlow = () => {

View File

@ -1,10 +1,21 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Spin } from 'antd'; import {
Button,
Divider,
Empty,
Flex,
Input,
Skeleton,
Space,
Spin,
} from 'antd';
import AgentTemplateModal from './agent-template-modal'; import AgentTemplateModal from './agent-template-modal';
import FlowCard from './flow-card'; import FlowCard from './flow-card';
import { useFetchDataOnMount, useSaveFlow } from './hooks'; import { useFetchDataOnMount, useSaveFlow } from './hooks';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useMemo } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
import styles from './index.less'; import styles from './index.less';
const FlowList = () => { const FlowList = () => {
@ -17,29 +28,66 @@ const FlowList = () => {
} = useSaveFlow(); } = useSaveFlow();
const { t } = useTranslate('flow'); const { t } = useTranslate('flow');
const { list, loading } = useFetchDataOnMount(); const {
data,
loading,
searchString,
handleInputChange,
fetchNextPage,
hasNextPage,
} = useFetchDataOnMount();
const nextList = useMemo(() => {
const list =
data?.pages?.flatMap((x) => (Array.isArray(x.kbs) ? x.kbs : [])) ?? [];
return list;
}, [data?.pages]);
const total = useMemo(() => {
return data?.pages.at(-1).total ?? 0;
}, [data?.pages]);
return ( return (
<Flex className={styles.flowListWrapper} vertical flex={1} gap={'large'}> <Flex className={styles.flowListWrapper} vertical flex={1} gap={'large'}>
<Flex justify={'end'}> <Flex justify={'end'}>
<Button <Space size={'large'}>
type="primary" <Input
icon={<PlusOutlined />} placeholder={t('searchAgentPlaceholder')}
onClick={showFlowSettingModal} value={searchString}
> style={{ width: 220 }}
{t('createGraph')} allowClear
</Button> onChange={handleInputChange}
prefix={<SearchOutlined />}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={showFlowSettingModal}
>
{t('createGraph')}
</Button>
</Space>
</Flex> </Flex>
<Spin spinning={loading}> <Spin spinning={loading}>
<Flex gap={'large'} wrap="wrap" className={styles.flowCardContainer}> <InfiniteScroll
{list.length > 0 ? ( dataLength={nextList?.length ?? 0}
list.map((item) => { next={fetchNextPage}
return <FlowCard item={item} key={item.id}></FlowCard>; hasMore={hasNextPage}
}) loader={<Skeleton avatar paragraph={{ rows: 1 }} active />}
) : ( endMessage={!!total && <Divider plain>{t('noMoreData')} 🤐</Divider>}
<Empty className={styles.knowledgeEmpty}></Empty> scrollableTarget="scrollableDiv"
)} >
</Flex> <Flex gap={'large'} wrap="wrap" className={styles.flowCardContainer}>
{nextList.length > 0 ? (
nextList.map((item) => {
return <FlowCard item={item} key={item.id}></FlowCard>;
})
) : (
<Empty className={styles.knowledgeEmpty}></Empty>
)}
</Flex>
</InfiniteScroll>
</Spin> </Spin>
{flowSettingVisible && ( {flowSettingVisible && (
<AgentTemplateModal <AgentTemplateModal

View File

@ -16,6 +16,8 @@ const {
testDbConnect, testDbConnect,
getInputElements, getInputElements,
debug, debug,
listCanvasTeam,
settingCanvas,
} = api; } = api;
const methods = { const methods = {
@ -71,6 +73,14 @@ const methods = {
url: debug, url: debug,
method: 'post', method: 'post',
}, },
listCanvasTeam: {
url: listCanvasTeam,
method: 'get',
},
settingCanvas: {
url: settingCanvas,
method: 'post',
},
} as const; } as const;
const flowService = registerServer<keyof typeof methods>(methods, request); const flowService = registerServer<keyof typeof methods>(methods, request);

View File

@ -123,10 +123,12 @@ export default {
// flow // flow
listTemplates: `${api_host}/canvas/templates`, listTemplates: `${api_host}/canvas/templates`,
listCanvas: `${api_host}/canvas/list`, listCanvas: `${api_host}/canvas/list`,
listCanvasTeam: `${api_host}/canvas/listteam`,
getCanvas: `${api_host}/canvas/get`, getCanvas: `${api_host}/canvas/get`,
getCanvasSSE: `${api_host}/canvas/getsse`, getCanvasSSE: `${api_host}/canvas/getsse`,
removeCanvas: `${api_host}/canvas/rm`, removeCanvas: `${api_host}/canvas/rm`,
setCanvas: `${api_host}/canvas/set`, setCanvas: `${api_host}/canvas/set`,
settingCanvas: `${api_host}/canvas/setting`,
getListVersion: `${api_host}/canvas/getlistversion`, getListVersion: `${api_host}/canvas/getlistversion`,
getVersion: `${api_host}/canvas/getversion`, getVersion: `${api_host}/canvas/getversion`,
resetCanvas: `${api_host}/canvas/reset`, resetCanvas: `${api_host}/canvas/reset`,