### 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
| 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 | ||||
| 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 | ||||
| @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): | ||||
| 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 | ||||
| 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 | ||||
| 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) |
| avatar = TextField(null=True, help_text="avatar base64 string") | avatar = TextField(null=True, help_text="avatar base64 string") | ||||
| 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={}) | ||||
| class Meta: | class Meta: | ||||
| db_table = "user_canvas" | db_table = "user_canvas" | ||||
| ) | ) | ||||
| 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 | |||||
| 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 | ||||
| agents = agents.paginate(page_number, items_per_page) | agents = agents.paginate(page_number, items_per_page) | ||||
| 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) |
| 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; | ||||
| 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 ?? []; | ||||
| }, | }, |
| 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 { |
| 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', | ||||
| version: 'Version', | version: 'Version', | ||||
| select: 'No version selected', | 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', | |||||
| }, | |||||
| }, | }, | ||||
| }, | }, | ||||
| }; | }; |
| s3: 'S3 上傳', | s3: 'S3 上傳', | ||||
| preview: '預覽', | preview: '預覽', | ||||
| fileError: '文件錯誤', | fileError: '文件錯誤', | ||||
| uploadLimit: '本地部署的單次上傳檔案總大小上限為 1GB,單次批量上傳檔案數不超過 32,單個帳戶不限檔案數量。', | |||||
| uploadLimit: | |||||
| '本地部署的單次上傳檔案總大小上限為 1GB,單次批量上傳檔案數不超過 32,單個帳戶不限檔案數量。', | |||||
| destinationFolder: '目標資料夾', | destinationFolder: '目標資料夾', | ||||
| }, | }, | ||||
| flow: { | flow: { |
| 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> | |||||
| ); | |||||
| }; |
| .flowHeader { | .flowHeader { | ||||
| padding: 10px 20px; | padding: 10px 20px; | ||||
| } | } | ||||
| .hideRibbon { | |||||
| display: none !important; | |||||
| } | |||||
| .ribbon { | |||||
| top: 4px; | |||||
| } |
| 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, | ||||
| 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(); | ||||
| 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(); | ||||
| } | } | ||||
| }, [getBeginNodeDataQuery, handleRun, showChatDrawer]); | }, [getBeginNodeDataQuery, handleRun, showChatDrawer]); | ||||
| const showSetting = useCallback(() => { | |||||
| setVisibleSettingMModal(true); | |||||
| }, [setVisibleSettingMModal]); | |||||
| const showListVersion = useCallback(() => { | const showListVersion = useCallback(() => { | ||||
| setVisibleHistoryVersionModal(true); | setVisibleHistoryVersionModal(true); | ||||
| }, [setVisibleHistoryVersionModal]); | }, [setVisibleHistoryVersionModal]); | ||||
| 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`}> | |||||
| <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> | <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> | ||||
| isAgent | isAgent | ||||
| ></EmbedModal> | ></EmbedModal> | ||||
| )} | )} | ||||
| {visibleSettingModal && ( | |||||
| <FlowSettingModal | |||||
| id={id || ''} | |||||
| visible={visibleSettingModal} | |||||
| hideModal={() => setVisibleSettingMModal(false)} | |||||
| ></FlowSettingModal> | |||||
| )} | |||||
| {visibleHistoryVersionModal && ( | {visibleHistoryVersionModal && ( | ||||
| <HistoryVersionModal | <HistoryVersionModal | ||||
| id={id || ''} | id={id || ''} |
| vertical-align: middle; | vertical-align: middle; | ||||
| } | } | ||||
| } | } | ||||
| .hideRibbon { | |||||
| display: none !important; | |||||
| } | |||||
| .ribbon { | |||||
| top: 4px; | |||||
| } |
| 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]); | ||||
| }; | }; | ||||
| return ( | 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> | ||||
| </div> | </div> | ||||
| </div> | |||||
| </Card> | |||||
| </Card> | |||||
| </Badge.Ribbon> | |||||
| ); | ); | ||||
| }; | }; | ||||
| import { useSetModalState } from '@/hooks/common-hooks'; | 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 { 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 = () => { |
| 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 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 = () => { | ||||
| } = 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 | |||||
| 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> | </Flex> | ||||
| <Spin spinning={loading}> | <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> | </Spin> | ||||
| {flowSettingVisible && ( | {flowSettingVisible && ( | ||||
| <AgentTemplateModal | <AgentTemplateModal |
| testDbConnect, | testDbConnect, | ||||
| getInputElements, | getInputElements, | ||||
| debug, | debug, | ||||
| listCanvasTeam, | |||||
| settingCanvas, | |||||
| } = api; | } = api; | ||||
| const methods = { | 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); |
| // 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`, |