| aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiPluginController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiPlugin.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| aiflowy-ui-react/src/components/Accordion/Accordion.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| aiflowy-ui-react/src/components/Accordion/accordion.css | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| aiflowy-ui-react/src/pages/ai/botDesign/PluginTools.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| aiflowy-ui-react/src/pages/ai/workflowDesign/WorkflowDesign.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| aiflowy-ui-react/src/pages/ai/workflowDesign/customNode/pluginNode.ts | ●●●●● 补丁 | 查看 | 原始文档 | 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: '插件',