### What problem does this PR solve? Allow member view agent # Canvas editor  # List agent  # Setting  _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>tags/v0.18.0
| @@ -18,6 +18,7 @@ import traceback | |||
| from flask import request, Response | |||
| from flask_login import login_required, current_user | |||
| 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.settings import RetCode | |||
| 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 peewee import MySQLDatabase, PostgresqlDatabase | |||
| from api.db.db_models import APIToken | |||
| import logging | |||
| import time | |||
| @manager.route('/templates', methods=['GET']) # noqa: F821 | |||
| @@ -86,10 +88,11 @@ def save(): | |||
| @manager.route('/get/<canvas_id>', methods=['GET']) # noqa: F821 | |||
| @login_required | |||
| 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: | |||
| 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 | |||
| def getsse(canvas_id): | |||
| @@ -288,10 +291,6 @@ def test_db_connect(): | |||
| return get_json_result(data="Database Connection Successful!") | |||
| except Exception as e: | |||
| return server_error_response(e) | |||
| #api get list version dsl of canvas | |||
| @manager.route('/getlistversion/<canvas_id>', methods=['GET']) # noqa: F821 | |||
| @login_required | |||
| @@ -301,7 +300,6 @@ def getlistversion(canvas_id): | |||
| return get_json_result(data=list) | |||
| except Exception as e: | |||
| return get_data_error_result(message=f"Error getting history files: {e}") | |||
| #api get version dsl of canvas | |||
| @manager.route('/getversion/<version_id>', methods=['GET']) # noqa: F821 | |||
| @login_required | |||
| @@ -313,3 +311,42 @@ def getversion( version_id): | |||
| return get_json_result(data=version.to_dict()) | |||
| except Exception as 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) | |||
| @@ -967,11 +967,17 @@ class UserCanvas(DataBaseModel): | |||
| avatar = TextField(null=True, help_text="avatar base64 string") | |||
| 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") | |||
| permission = CharField( | |||
| max_length=16, | |||
| null=False, | |||
| help_text="me|team", | |||
| default="me", | |||
| index=True) | |||
| description = TextField(null=True, help_text="Canvas description") | |||
| canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True) | |||
| dsl = JSONField(null=True, default={}) | |||
| class Meta: | |||
| db_table = "user_canvas" | |||
| @@ -1140,3 +1146,11 @@ def migrate_db(): | |||
| ) | |||
| except Exception: | |||
| 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 | |||
| @@ -18,12 +18,13 @@ import time | |||
| import traceback | |||
| from uuid import uuid4 | |||
| 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.common_service import CommonService | |||
| from api.db.services.conversation_service import structure_answer | |||
| from api.utils import get_uuid | |||
| from peewee import fn | |||
| class CanvasTemplateService(CommonService): | |||
| model = CanvasTemplate | |||
| @@ -50,7 +51,74 @@ class UserCanvasService(CommonService): | |||
| agents = agents.paginate(page_number, items_per_page) | |||
| 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): | |||
| e, cvs = UserCanvasService.get_by_id(agent_id) | |||
| @@ -171,6 +171,27 @@ export const useFetchFlow = (): { | |||
| 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 = (): { | |||
| data: IFlow; | |||
| loading: boolean; | |||
| @@ -244,7 +265,9 @@ export const useDeleteFlow = () => { | |||
| mutationFn: async (canvasIds: string[]) => { | |||
| const { data } = await flowService.removeCanvas({ canvasIds }); | |||
| if (data.code === 0) { | |||
| queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] }); | |||
| queryClient.invalidateQueries({ | |||
| queryKey: ['infiniteFetchFlowListTeam'], | |||
| }); | |||
| } | |||
| return data?.data ?? []; | |||
| }, | |||
| @@ -37,6 +37,8 @@ export declare interface IFlow { | |||
| update_date: string; | |||
| update_time: number; | |||
| user_id: string; | |||
| permission: string; | |||
| nickname: string; | |||
| } | |||
| export interface IFlowTemplate { | |||
| @@ -1192,6 +1192,15 @@ This delimiter is used to split the input text into several text pieces echo of | |||
| addCategory: 'Add category', | |||
| categoryName: 'Category name', | |||
| 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', | |||
| insertVariableTip: `Enter / Insert variables`, | |||
| historyversion: 'History version', | |||
| @@ -1204,14 +1213,25 @@ This delimiter is used to split the input text into several text pieces echo of | |||
| version: 'Version', | |||
| select: 'No version selected', | |||
| }, | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| }, | |||
| layout: { | |||
| file: 'file', | |||
| knowledge: 'knowledge', | |||
| chat: 'chat', | |||
| setting: 'Setting', | |||
| settings: { | |||
| upload: 'Upload', | |||
| photo: 'Photo', | |||
| permissions: 'Permission', | |||
| permissionsTip: 'You can set the permissions of the team members here.', | |||
| me: 'me', | |||
| team: 'Team', | |||
| }, | |||
| noMoreData: 'No more data', | |||
| searchAgentPlaceholder: 'Search agent', | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| }, | |||
| layout: { | |||
| file: 'file', | |||
| knowledge: 'knowledge', | |||
| chat: 'chat', | |||
| }, | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -722,7 +722,8 @@ export default { | |||
| s3: 'S3 上傳', | |||
| preview: '預覽', | |||
| fileError: '文件錯誤', | |||
| uploadLimit: '本地部署的單次上傳檔案總大小上限為 1GB,單次批量上傳檔案數不超過 32,單個帳戶不限檔案數量。', | |||
| uploadLimit: | |||
| '本地部署的單次上傳檔案總大小上限為 1GB,單次批量上傳檔案數不超過 32,單個帳戶不限檔案數量。', | |||
| destinationFolder: '目標資料夾', | |||
| }, | |||
| flow: { | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -1,3 +1,10 @@ | |||
| .flowHeader { | |||
| padding: 10px 20px; | |||
| } | |||
| .hideRibbon { | |||
| display: none !important; | |||
| } | |||
| .ribbon { | |||
| top: 4px; | |||
| } | |||
| @@ -3,10 +3,13 @@ import { useShowEmbedModal } from '@/components/api-service/hooks'; | |||
| import { SharedFrom } from '@/constants/chat'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useFetchFlow } from '@/hooks/flow-hooks'; | |||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||
| 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 { Link, useParams } from 'umi'; | |||
| import { FlowSettingModal, useFlowSettingModal } from '../flow-setting'; | |||
| import { | |||
| useGetBeginNodeDataQuery, | |||
| useGetBeginNodeDataQueryIsSafe, | |||
| @@ -32,6 +35,8 @@ interface IProps { | |||
| const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | |||
| const { saveGraph } = useSaveGraph(); | |||
| const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); | |||
| const { data: userInfo } = useFetchUserInfo(); | |||
| const { data } = useFetchFlow(); | |||
| const { t } = useTranslate('flow'); | |||
| const { id } = useParams(); | |||
| @@ -39,6 +44,8 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | |||
| const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); | |||
| const { showEmbedModal, hideEmbedModal, embedVisible, beta } = | |||
| useShowEmbedModal(); | |||
| const { setVisibleSettingMModal, visibleSettingModal } = | |||
| useFlowSettingModal(); | |||
| const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe(); | |||
| const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } = | |||
| useHistoryVersionModal(); | |||
| @@ -55,6 +62,10 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | |||
| } | |||
| }, [getBeginNodeDataQuery, handleRun, showChatDrawer]); | |||
| const showSetting = useCallback(() => { | |||
| setVisibleSettingMModal(true); | |||
| }, [setVisibleSettingMModal]); | |||
| const showListVersion = useCallback(() => { | |||
| setVisibleHistoryVersionModal(true); | |||
| }, [setVisibleHistoryVersionModal]); | |||
| @@ -66,31 +77,56 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | |||
| gap={'large'} | |||
| 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'}> | |||
| <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> | |||
| <Space size={'large'}> | |||
| <Button onClick={handleRunAgent}> | |||
| <Button | |||
| disabled={userInfo.nickname !== data.nickname} | |||
| onClick={handleRunAgent} | |||
| > | |||
| <b>{t('run')}</b> | |||
| </Button> | |||
| <Button type="primary" onClick={() => saveGraph()}> | |||
| <Button | |||
| disabled={userInfo.nickname !== data.nickname} | |||
| type="primary" | |||
| onClick={() => saveGraph()} | |||
| > | |||
| <b>{t('save')}</b> | |||
| </Button> | |||
| <Button | |||
| type="primary" | |||
| onClick={handleShowEmbedModal} | |||
| disabled={!isBeginNodeDataQuerySafe} | |||
| disabled={ | |||
| !isBeginNodeDataQuerySafe || userInfo.nickname !== data.nickname | |||
| } | |||
| > | |||
| <b>{t('embedIntoSite', { keyPrefix: 'common' })}</b> | |||
| </Button> | |||
| <Button | |||
| disabled={userInfo.nickname !== data.nickname} | |||
| type="primary" | |||
| onClick={showSetting} | |||
| > | |||
| <b>{t('setting')}</b> | |||
| </Button> | |||
| <Button type="primary" onClick={showListVersion}> | |||
| <b>{t('historyversion')}</b> | |||
| </Button> | |||
| @@ -106,6 +142,13 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | |||
| isAgent | |||
| ></EmbedModal> | |||
| )} | |||
| {visibleSettingModal && ( | |||
| <FlowSettingModal | |||
| id={id || ''} | |||
| visible={visibleSettingModal} | |||
| hideModal={() => setVisibleSettingMModal(false)} | |||
| ></FlowSettingModal> | |||
| )} | |||
| {visibleHistoryVersionModal && ( | |||
| <HistoryVersionModal | |||
| id={id || ''} | |||
| @@ -74,3 +74,11 @@ | |||
| vertical-align: middle; | |||
| } | |||
| } | |||
| .hideRibbon { | |||
| display: none !important; | |||
| } | |||
| .ribbon { | |||
| top: 4px; | |||
| } | |||
| @@ -1,22 +1,26 @@ | |||
| import { formatDate } from '@/utils/date'; | |||
| import { CalendarOutlined } from '@ant-design/icons'; | |||
| import { Card, Typography } from 'antd'; | |||
| import { Badge, Card, Typography } from 'antd'; | |||
| import { useNavigate } from 'umi'; | |||
| import OperateDropdown from '@/components/operate-dropdown'; | |||
| import { useDeleteFlow } from '@/hooks/flow-hooks'; | |||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||
| import { IFlow } from '@/interfaces/database/flow'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback } from 'react'; | |||
| import GraphAvatar from '../graph-avatar'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| item: IFlow; | |||
| onDelete?: (string: string) => void; | |||
| } | |||
| const FlowCard = ({ item }: IProps) => { | |||
| const navigate = useNavigate(); | |||
| const { deleteFlow } = useDeleteFlow(); | |||
| const { data: userInfo } = useFetchUserInfo(); | |||
| const removeFlow = useCallback(() => { | |||
| return deleteFlow([item.id]); | |||
| @@ -27,33 +31,41 @@ const FlowCard = ({ item }: IProps) => { | |||
| }; | |||
| return ( | |||
| <Card className={styles.card} onClick={handleCardClick}> | |||
| <div className={styles.container}> | |||
| <div className={styles.content}> | |||
| <GraphAvatar avatar={item.avatar}></GraphAvatar> | |||
| <OperateDropdown deleteItem={removeFlow}></OperateDropdown> | |||
| </div> | |||
| <div className={styles.titleWrapper}> | |||
| <Typography.Title | |||
| className={styles.title} | |||
| ellipsis={{ tooltip: item.title }} | |||
| > | |||
| {item.title} | |||
| </Typography.Title> | |||
| <p>{item.description}</p> | |||
| </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> | |||
| <Badge.Ribbon | |||
| text={item?.nickname} | |||
| color={userInfo?.nickname === item?.nickname ? '#1677ff' : 'pink'} | |||
| className={classNames(styles.ribbon, { | |||
| [styles.hideRibbon]: item.permission !== 'team', | |||
| })} | |||
| > | |||
| <Card className={styles.card} onClick={handleCardClick}> | |||
| <div className={styles.container}> | |||
| <div className={styles.content}> | |||
| <GraphAvatar avatar={item.avatar}></GraphAvatar> | |||
| <OperateDropdown deleteItem={removeFlow}></OperateDropdown> | |||
| </div> | |||
| <div className={styles.titleWrapper}> | |||
| <Typography.Title | |||
| className={styles.title} | |||
| ellipsis={{ tooltip: item.title }} | |||
| > | |||
| {item.title} | |||
| </Typography.Title> | |||
| <p>{item.description}</p> | |||
| </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> | |||
| </Card> | |||
| </Card> | |||
| </Badge.Ribbon> | |||
| ); | |||
| }; | |||
| @@ -1,16 +1,56 @@ | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { | |||
| useFetchFlowList, | |||
| useFetchFlowTemplates, | |||
| useSetFlow, | |||
| } from '@/hooks/flow-hooks'; | |||
| import { useFetchFlowTemplates, useSetFlow } from '@/hooks/flow-hooks'; | |||
| import { useHandleSearchChange } from '@/hooks/logic-hooks'; | |||
| import flowService from '@/services/flow-service'; | |||
| import { useInfiniteQuery } from '@tanstack/react-query'; | |||
| import { useDebounce } from 'ahooks'; | |||
| import { useCallback } from 'react'; | |||
| import { useNavigate } from 'umi'; | |||
| 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 = () => { | |||
| @@ -1,10 +1,21 @@ | |||
| import { PlusOutlined } from '@ant-design/icons'; | |||
| import { Button, Empty, Flex, Spin } from 'antd'; | |||
| import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; | |||
| import { | |||
| Button, | |||
| Divider, | |||
| Empty, | |||
| Flex, | |||
| Input, | |||
| Skeleton, | |||
| Space, | |||
| Spin, | |||
| } from 'antd'; | |||
| import AgentTemplateModal from './agent-template-modal'; | |||
| import FlowCard from './flow-card'; | |||
| import { useFetchDataOnMount, useSaveFlow } from './hooks'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useMemo } from 'react'; | |||
| import InfiniteScroll from 'react-infinite-scroll-component'; | |||
| import styles from './index.less'; | |||
| const FlowList = () => { | |||
| @@ -17,29 +28,66 @@ const FlowList = () => { | |||
| } = useSaveFlow(); | |||
| 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 ( | |||
| <Flex className={styles.flowListWrapper} vertical flex={1} gap={'large'}> | |||
| <Flex justify={'end'}> | |||
| <Button | |||
| type="primary" | |||
| icon={<PlusOutlined />} | |||
| onClick={showFlowSettingModal} | |||
| > | |||
| {t('createGraph')} | |||
| </Button> | |||
| <Space size={'large'}> | |||
| <Input | |||
| placeholder={t('searchAgentPlaceholder')} | |||
| value={searchString} | |||
| style={{ width: 220 }} | |||
| allowClear | |||
| onChange={handleInputChange} | |||
| prefix={<SearchOutlined />} | |||
| /> | |||
| <Button | |||
| type="primary" | |||
| icon={<PlusOutlined />} | |||
| onClick={showFlowSettingModal} | |||
| > | |||
| {t('createGraph')} | |||
| </Button> | |||
| </Space> | |||
| </Flex> | |||
| <Spin spinning={loading}> | |||
| <Flex gap={'large'} wrap="wrap" className={styles.flowCardContainer}> | |||
| {list.length > 0 ? ( | |||
| list.map((item) => { | |||
| return <FlowCard item={item} key={item.id}></FlowCard>; | |||
| }) | |||
| ) : ( | |||
| <Empty className={styles.knowledgeEmpty}></Empty> | |||
| )} | |||
| </Flex> | |||
| <InfiniteScroll | |||
| dataLength={nextList?.length ?? 0} | |||
| next={fetchNextPage} | |||
| hasMore={hasNextPage} | |||
| loader={<Skeleton avatar paragraph={{ rows: 1 }} active />} | |||
| endMessage={!!total && <Divider plain>{t('noMoreData')} 🤐</Divider>} | |||
| scrollableTarget="scrollableDiv" | |||
| > | |||
| <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> | |||
| {flowSettingVisible && ( | |||
| <AgentTemplateModal | |||
| @@ -16,6 +16,8 @@ const { | |||
| testDbConnect, | |||
| getInputElements, | |||
| debug, | |||
| listCanvasTeam, | |||
| settingCanvas, | |||
| } = api; | |||
| const methods = { | |||
| @@ -71,6 +73,14 @@ const methods = { | |||
| url: debug, | |||
| method: 'post', | |||
| }, | |||
| listCanvasTeam: { | |||
| url: listCanvasTeam, | |||
| method: 'get', | |||
| }, | |||
| settingCanvas: { | |||
| url: settingCanvas, | |||
| method: 'post', | |||
| }, | |||
| } as const; | |||
| const flowService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -123,10 +123,12 @@ export default { | |||
| // flow | |||
| listTemplates: `${api_host}/canvas/templates`, | |||
| listCanvas: `${api_host}/canvas/list`, | |||
| listCanvasTeam: `${api_host}/canvas/listteam`, | |||
| getCanvas: `${api_host}/canvas/get`, | |||
| getCanvasSSE: `${api_host}/canvas/getsse`, | |||
| removeCanvas: `${api_host}/canvas/rm`, | |||
| setCanvas: `${api_host}/canvas/set`, | |||
| settingCanvas: `${api_host}/canvas/setting`, | |||
| getListVersion: `${api_host}/canvas/getlistversion`, | |||
| getVersion: `${api_host}/canvas/getversion`, | |||
| resetCanvas: `${api_host}/canvas/reset`, | |||