zhangjinyang
2025-05-15 ac54c36e2bf4e024569719f7b6cf131b475ed794
refactor: 重构插件选择页面。
5个文件已修改
2个文件已添加
270 ■■■■ 已修改文件
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiPluginController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiPlugin.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/components/Accordion/Accordion.tsx 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/components/Accordion/accordion.css 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/botDesign/PluginTools.tsx 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/workflowDesign/WorkflowDesign.tsx 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/workflowDesign/customNode/pluginNode.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiPluginController.java
@@ -1,5 +1,7 @@
package tech.aiflowy.ai.controller;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.PostMapping;
import tech.aiflowy.common.domain.Result;
import tech.aiflowy.common.web.controller.BaseCurdController;
@@ -57,4 +59,9 @@
    public Result getList(){
        return aiPluginService.getList();
    }
    @Override
    protected Page<AiPlugin> queryPage(Page<AiPlugin> page, QueryWrapper queryWrapper) {
        return service.getMapper().paginateWithRelations(page, queryWrapper);
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiPlugin.java
@@ -1,8 +1,10 @@
package tech.aiflowy.ai.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.RelationOneToMany;
import com.mybatisflex.annotation.Table;
import tech.aiflowy.ai.entity.base.AiPluginBase;
import tech.aiflowy.ai.mapper.AiPluginToolMapper;
import java.util.List;
@@ -16,9 +18,18 @@
@Table("tb_ai_plugin")
public class AiPlugin extends AiPluginBase {
    @RelationOneToMany(selfField = "id", targetField = "pluginId", targetTable = "tb_ai_plugin_tool")
    private List<AiPluginTool> tools;
    public String getTitle() {
        return this.getName();
    }
    public List<AiPluginTool> getTools() {
        return tools;
    }
    public void setTools(List<AiPluginTool> tools) {
        this.tools = tools;
    }
}
aiflowy-ui-react/src/components/Accordion/Accordion.tsx
New file
@@ -0,0 +1,40 @@
import React from 'react';
import {DownOutlined, RightOutlined} from "@ant-design/icons";
import {Avatar, List} from "antd";
/**
 * 手风琴面板项
 */
export type AccordionItemType = {
    title: string,
    children?: React.ReactNode,
    isActive?: boolean|false,
    clickItem: () => void
}
const AccordionItem: React.FC<AccordionItemType> = ({ title, children, isActive, clickItem }) => {
    return (
        <div className={`accordion-item ${isActive ? 'active' : ''}`}>
            <div className="accordion-header" onClick={clickItem}>
                <List style={{width: "100%"}}>
                    <List.Item>
                        <List.Item.Meta
                            avatar={<Avatar src={`https://api.dicebear.com/7.x/miniavs/svg?seed=${1}`} />}
                            title={title}
                            description={"dsadsahgjdshgfjdjh"}
                        />
                    </List.Item>
                </List>
                <span className="accordion-icon">
          {isActive ? <DownOutlined style={{fontSize: '12px'}} /> : <RightOutlined style={{fontSize: '12px'}} />}
        </span>
            </div>
            {isActive && (
                <div className="accordion-content">
                    {children}
                </div>
            )}
        </div>
    );
};
export { AccordionItem };
aiflowy-ui-react/src/components/Accordion/accordion.css
New file
@@ -0,0 +1,44 @@
.accordion {
    /*border-bottom: 1px solid #ddd;*/
    /*border-radius: 4px;*/
    overflow: hidden;
}
.accordion-item {
    border-bottom: 1px solid #ebebeb;
}
.accordion-item.active {
}
.accordion-item:last-child {
    border-bottom: none;
}
.accordion-header {
    padding: 15px;
    background-color: #f5f5f5;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
    transition: background-color 0.3s;
}
.accordion-header:hover {
    background-color: #e9e9e9;
}
.accordion-header h3 {
    margin: 0;
    font-size: 16px;
}
.accordion-icon {
    font-size: 18px;
    font-weight: bold;
}
.accordion-content {
    padding: 15px;
    background-color: #f9f9f9;
}
aiflowy-ui-react/src/pages/ai/botDesign/PluginTools.tsx
@@ -1,58 +1,122 @@
import React, {useState} from "react";
import {Button, Col, List, Modal, Row} from "antd";
import {Button, Col, Empty, List, Modal, Pagination, Row, Spin} from "antd";
import {ModalProps} from "antd/es/modal/interface";
import './pluginTool.css'
import {useGet, useGetManual} from "../../../hooks/useApis.ts";
import {useGet} from "../../../hooks/useApis.ts";
import Search from "antd/es/input/Search";
import {useNavigate} from "react-router-dom";
import {AccordionItem} from '../../../components/Accordion/Accordion.tsx';
import '../../../components/Accordion/accordion.css'
export type PluginToolsProps = {
    selectedItem?: any[],
    goToPage: string,
    onSelectedItem?: (item: any) => void
    onRemoveItem?: (item: any) => void
} & ModalProps
export const PluginTools: React.FC<PluginToolsProps> = (props) => {
    const selected = props.selectedItem;
    const [pageParams, setPageParams] = useState({
        pageNumber: 1,
        pageSize: 10,
        name: ''
    });
    const [selectedIndex, setSelectedIndex] = useState(-1);
    const {result: data} = useGet('/api/v1/aiPlugin/list')
    const {loading, result: toolsData, doGet: getTools} = useGetManual('/api/v1/aiPluginTool/list')
    const {loading: pageDataLoading, result: pageData, doGet: getPageData} = useGet('/api/v1/aiPlugin/page', {
        pageNumber: pageParams.pageNumber,
        pageSize: pageParams.pageSize
    })
    const navigate = useNavigate();
    const goToPage = props.goToPage;
    const onSearch = (value: string) => {
        setPageParams({
            ...pageParams,
            name: value
        })
        getPageData({
            params: {
                pageNumber: 1,
                pageSize: pageParams.pageSize,
                name: value
            }
        })
    };
    const changeCurrent = (page: number) => {
        setPageParams({
            ...pageParams,
            pageNumber: page,
            pageSize: pageParams.pageSize
        })
        getPageData({
            params: {
                pageNumber: page,
                pageSize: pageParams.pageSize
            }
        })
    }
    return (
        <Modal title={'选择插件'} footer={null} {...props} width={"800px"}
        <Modal title={'选择插件'} footer={null} {...props} width={"1000px"}
               height={"600px"}>
            <Row gutter={16} style={{width: "100%"}}>
                <Col span={6} style={{borderRight: "1px solid #e2e2e2"}}>
                    {data?.data.map((item: any, index: number) => {
                        return (
                            <div key={index} className={`main-item ${index == selectedIndex ? 'active' : ''}`}
                                 onClick={() => {
                                     getTools({
                                         params: {
                                             pluginId: item.id
                                         }
                                     })
                                     setSelectedIndex(index)
                                 }}>
                                {item.name}
                            </div>)
                    })
                    }
                <Col span={6} style={{backgroundColor: "#f5f5f5", paddingTop: "10px"}}>
                    <Search style={{marginBottom: "10px"}} placeholder="搜索" onSearch={onSearch}/>
                    <Button block type={"primary"} onClick={() => {
                        goToPage ? navigate(goToPage) : ''
                    }}>创建插件</Button>
                </Col>
                <Col span={18}>
                    <List
                        loading={loading}
                        dataSource={toolsData?.data}
                        renderItem={(item: any, index) => (
                            <List.Item
                                key={index}
                                actions={[<Button onClick={() => {
                                    props.onSelectedItem?.(item)
                                }}>选择</Button>]}
                            >
                                <List.Item.Meta
                                    title={item.name}
                                    description={item.description}
                                />
                            </List.Item>
                        )}
                    />
                    <Spin spinning={pageDataLoading}>
                        <div>
                            {pageData?.data.totalRow > 0 ? pageData?.data.records.map((item: any, index: number) => {
                                return (
                                    <AccordionItem key={index} title={item.name} isActive={selectedIndex == index}
                                                   clickItem={() => {
                                                       if (selectedIndex == index) {
                                                           setSelectedIndex(-1)
                                                       } else {
                                                           setSelectedIndex(index)
                                                       }
                                                   }}>
                                        <List
                                            dataSource={item.tools}
                                            renderItem={(item: any, index) => (
                                                <List.Item
                                                    key={index}
                                                    actions={selected?.includes(item.id)?[<Button color="danger" variant="outlined" onClick={() => {
                                                        props.onRemoveItem?.(item)}
                                                    }>
                                                        移除
                                                    </Button>]:[<Button onClick={() => {
                                                        props.onSelectedItem?.(item)}
                                                    }>
                                                        选择
                                                    </Button>]}
                                                >
                                                    <List.Item.Meta
                                                        title={item.name}
                                                        description={item.description}
                                                    />
                                                </List.Item>
                                            )}
                                        />
                                    </AccordionItem>
                                )
                            }) : <Empty style={{height: "100%"}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
                            }
                            <div style={{padding: "10px", backgroundColor: "#f5f5f5"}}>
                                <Pagination onChange={changeCurrent} align="end" defaultCurrent={pageParams.pageNumber}
                                            pageSize={pageParams.pageSize} total={pageData?.data.totalRow}/>
                            </div>
                        </div>
                    </Spin>
                </Col>
            </Row>
        </Modal>
aiflowy-ui-react/src/pages/ai/workflowDesign/WorkflowDesign.tsx
@@ -109,7 +109,7 @@
    const {doGet: getRunningParameters} = useGetManual("/api/v1/aiWorkflow/getRunningParameters");
    const {doPost: tryRunning} = usePostManual("/api/v1/aiWorkflow/tryRunning");
    const [selectedItem, setSelectedItem] = useState<any>([]);
    const showRunningParameters = async () => {
        setRunLoading(true)
        await doUpdate({
@@ -166,7 +166,10 @@
    const [changeNodeData, setChangeNodeData] = useState<any>()
    const handleChosen = (updateNodeData: any) => {
    const handleChosen = (updateNodeData: any,  value: any) => {
        if (value) {
            setSelectedItem([value])
        }
        setChangeNodeData(() => updateNodeData)
        setPluginOpen(true)
    }
@@ -188,6 +191,8 @@
    return (
        <>
            <PluginTools
                selectedItem={selectedItem}
                goToPage="/ai/plugin"
                open={pluginOpen} onClose={() => setPluginOpen(false)}
                onCancel={() => setPluginOpen(false)}
                onSelectedItem={item => {
@@ -204,7 +209,22 @@
                            changeNodeData(res.data.data)
                        })
                    }
                }}/>
                    setSelectedItem([item.id])
                }}
                onRemoveItem={() => {
                    setPluginOpen(false)
                    // 调用保存的 updateNodeData 函数
                    if (changeNodeData) {
                        changeNodeData({
                            pluginId: '',
                            pluginName: '',
                            parameters:[],
                            outputDefs: []
                        })
                        setSelectedItem([])
                    }
                }}
            />
            <Drawer
                width={640}
                title="请输入参数"
aiflowy-ui-react/src/pages/ai/workflowDesign/customNode/pluginNode.ts
@@ -1,5 +1,5 @@
export interface PluginNodeOptions {
    onChosen?: (updateNodeData: any) => void;
    onChosen?: (updateNodeData: any,value: string) => void;
}
export const PluginNode = (options: PluginNodeOptions = {}) => ({
    title: '插件',