-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: [Intelligent Agent] Workflow intelligent agent adds workflow component search/location function #4822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| <template> | ||
| <div> | ||
| <!-- 搜索遮罩层 --> | ||
| <Teleport to="body"> | ||
| <div v-if="showSearch" class="search-mask" @click.self="closeSearch"> | ||
| <div class="search-container"> | ||
| <el-input | ||
| ref="searchInputRef" | ||
| v-model="searchText" | ||
| placeholder="搜索..." | ||
| :prefix-icon="Search" | ||
| clearable | ||
| @keyup.enter="handleSearch" | ||
| @keyup.esc="closeSearch" | ||
| > | ||
| <template #append> | ||
| <el-button @click="closeSearch">取消</el-button> | ||
| </template> | ||
| </el-input> | ||
| </div> | ||
| </div> | ||
| </Teleport> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { ref, onMounted, onUnmounted, nextTick } from 'vue' | ||
| import { Search } from '@element-plus/icons-vue' | ||
|
|
||
| // Props定义 | ||
| interface Props { | ||
| onSearch?: (keyword: string) => void // 搜索回调 | ||
| } | ||
|
|
||
| const props = withDefaults(defineProps<Props>(), { | ||
| useElementPlus: false, | ||
| onSearch: undefined, | ||
| }) | ||
|
|
||
| // 状态 | ||
| const showSearch = ref(false) | ||
| const searchText = ref('') | ||
| const searchInputRef = ref<any>(null) | ||
| const nativeInputRef = ref<HTMLInputElement | null>(null) | ||
|
|
||
| // 快捷键处理 | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| // Ctrl+F 或 Cmd+F (Mac) | ||
| if ((e.ctrlKey || e.metaKey) && e.key === 'f') { | ||
| e.preventDefault() // 阻止浏览器默认搜索 | ||
| openSearch() | ||
| } | ||
|
|
||
| // 按ESC关闭 | ||
| if (e.key === 'Escape' && showSearch.value) { | ||
| closeSearch() | ||
| } | ||
| } | ||
|
|
||
| // 打开搜索 | ||
| const openSearch = () => { | ||
| showSearch.value = true | ||
| searchText.value = '' | ||
|
|
||
| nextTick(() => { | ||
| searchInputRef.value?.focus() | ||
| }) | ||
| } | ||
|
|
||
| // 关闭搜索 | ||
| const closeSearch = () => { | ||
| showSearch.value = false | ||
| searchText.value = '' | ||
| } | ||
|
|
||
| // 执行搜索 | ||
| const handleSearch = () => { | ||
| if (searchText.value.trim()) { | ||
| props.onSearch?.(searchText.value) | ||
| } | ||
| } | ||
|
|
||
| // 生命周期 | ||
| onMounted(() => { | ||
| window.addEventListener('keydown', handleKeyDown) | ||
| }) | ||
|
|
||
| onUnmounted(() => { | ||
| window.removeEventListener('keydown', handleKeyDown) | ||
| }) | ||
| </script> | ||
|
|
||
| <style scoped> | ||
| .search-mask { | ||
| position: fixed; | ||
| top: 0; | ||
| left: 0; | ||
| right: 0; | ||
| bottom: 0; | ||
| background: rgba(0, 0, 0, 0.3); | ||
| display: flex; | ||
| justify-content: center; | ||
| z-index: 9999; | ||
| padding-top: 20vh; | ||
| } | ||
|
|
||
| .search-container { | ||
| width: 500px; | ||
| max-width: 90%; | ||
| animation: slideDown 0.2s ease; | ||
| } | ||
|
|
||
| /* 原生输入框样式 */ | ||
| .native-search { | ||
| display: flex; | ||
| gap: 8px; | ||
| background: white; | ||
| padding: 16px; | ||
| border-radius: 8px; | ||
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| } | ||
|
|
||
| .native-search input { | ||
| flex: 1; | ||
| padding: 10px 12px; | ||
| border: 1px solid #dcdfe6; | ||
| border-radius: 4px; | ||
| font-size: 14px; | ||
| outline: none; | ||
| } | ||
|
|
||
| .native-search input:focus { | ||
| border-color: #409eff; | ||
| } | ||
|
|
||
| .native-search button { | ||
| padding: 0 16px; | ||
| background: white; | ||
| border: 1px solid #dcdfe6; | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| transition: all 0.2s; | ||
| } | ||
|
|
||
| .native-search button:hover { | ||
| border-color: #409eff; | ||
| color: #409eff; | ||
| } | ||
|
|
||
| .content { | ||
| padding: 20px; | ||
| } | ||
|
|
||
| .item { | ||
| padding: 8px; | ||
| border-bottom: 1px solid #eee; | ||
| } | ||
|
|
||
| @keyframes slideDown { | ||
| from { | ||
| opacity: 0; | ||
| transform: translateY(-20px); | ||
| } | ||
| to { | ||
| opacity: 1; | ||
| transform: translateY(0); | ||
| } | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| <!-- 辅助工具栏 --> | ||
| <Control class="workflow-control" v-if="lf" :lf="lf"></Control> | ||
| <TeleportContainer :flow-id="flowId" /> | ||
| <NodeSearch :on-search="onSearch"></NodeSearch> | ||
| </template> | ||
| <script setup lang="ts"> | ||
| import LogicFlow from '@logicflow/core' | ||
|
|
@@ -17,6 +18,8 @@ import { initDefaultShortcut } from '@/workflow/common/shortcut' | |
| import Dagre from '@/workflow/plugins/dagre' | ||
| import { disconnectAll, getTeleport } from '@/workflow/common/teleport' | ||
| import { WorkflowMode } from '@/enums/application' | ||
| import { MsgSuccess, MsgWarning } from '@/utils/message' | ||
| import NodeSearch from '@/workflow/common/NodeSearch.vue' | ||
| const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true }) | ||
| const workflow_mode = inject('workflowMode') || WorkflowMode.Application | ||
| const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.ApplicationLoop | ||
|
|
@@ -49,6 +52,44 @@ onUnmounted(() => { | |
| const render = (data: any) => { | ||
| lf.value.render(data) | ||
| } | ||
| const searchQueue: Array<string> = [] | ||
| const selectNode = (node: any) => { | ||
| lf.value.graphModel.selectNodeById(node.id) | ||
| lf.value.graphModel.transformModel.focusOn( | ||
| node.x, | ||
| node.y, | ||
| lf.value.container.clientWidth, | ||
| lf.value.container.clientHeight, | ||
| ) | ||
| searchQueue.push(node.id) | ||
| } | ||
| const onSearch = (kw: string) => { | ||
| const graph_data = lf.value.getGraphData() | ||
| for (let index = 0; index < graph_data.nodes.length; index++) { | ||
| const node = graph_data.nodes[index] | ||
| let firstNode = null | ||
| if (node.properties.stepName.includes(kw)) { | ||
| if (!firstNode) { | ||
| firstNode = node | ||
| } | ||
|
|
||
| if (!searchQueue.includes(node.id)) { | ||
| selectNode(node) | ||
| break | ||
| } | ||
| } | ||
| if (index === graph_data.nodes.length - 1) { | ||
| searchQueue.length = 0 | ||
| if (firstNode) { | ||
| selectNode(firstNode) | ||
| } else { | ||
| lf.value.graphModel.clearSelectElements() | ||
| MsgWarning('不存在的节点') | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const renderGraphData = (data?: any) => { | ||
| const container: any = document.querySelector('#container') | ||
| if (container) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The provided code looks mostly correct but there are a few areas where improvements can be made:
Here’s an improved version of your code with some suggestions: <!-- 辅助工具栏 -->
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
<TeleportContainer :flow-id="flowId" />
<NodeSearch :on-search="onSearch"></NodeSearch>
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
import { WorkflowMode } from '@/enums/application'
import { MsgSuccess, MsgWarning } from '@/utils/message'
// Type definitions
interface Node {
id: string;
properties: Record<string, any>;
}
type GraphData = {
nodes: Node[];
};
// Setup data...
const lf = ref(null);
const flowId = 'your-flow-id';
const workflow_mode = inject('workflowMode') || WorkflowMode.Application;
const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.Application;
// Search-related variables
const searchQueue: Set<string> = new Set();
const selectNode = (node: Node) => {
lf.value.graphModel.selectNodeById(node.id);
lf.value.graphModel.transformModel.focusOn(
node.x,
node.y,
lf.value.container?.clientWidth ?? 0,
lf.value.container?.clientHeight ?? 0,
);
searchQueue.add(node.id);
};
const onSearch = async (kw: string) => {
try {
const graph_data = await lf.value.getGraphData(); // Assuming this returns Promise<object>
for (const node of graph_data.nodes) {
if (
node.properties &&
node.properties.stepName.toLowerCase().includes(kw.toLowerCase())
) {
await selectNode(node); // Await here since we change state inside selectNode
return; // Only show the first match
}
}
searchQueue.clear();
lf.value.graphModel.clearSelectElements();
MsgWarning('不存在的节点');
} catch (error) {
console.error("Failed to fetch graph data:", error);
MsgWarning('获取图形数据失败,请重试。');
}
};
const renderGraphData = (data?: GraphData) => {
const container: HTMLElement | null = document.getElementById('container');
if (container) {
// Your rendering logic here
}
};
</script>
<style scoped>
/* Your styles here */
</style>Key Changes:
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks generally clean and well-structured. Here are some minor suggestions for optimization and considerations:
TypeScript Typing: The
propsinterface is defined using TypeScript type annotations for clarity.Arrow Functions: Arrow functions (
()=>{}) can make the code slightly cleaner and more concise, especially when used in callback functions like.addEventListener()and.nextTick(). However, standard function declarations (function(){}) without an arrow might be suitable depending on personal preference or context.Comments: Comments can sometimes clutter the code and become outdated. Consider updating comments, removing unnecessary ones, or grouping similar comments together.
Avoid Global Event Listeners: Although necessary for this specific functionality, consider encapsulating event handlers within component state management if possible to avoid adding global listeners that could potentially interfere with other components or lead to memory leaks under certain conditions.
Optimize State Management: Ensure that the state variables are properly managed and updated only when necessary. For example, you might want to debounce updates to improve performance.
Overall, the code is straightforward and should work correctly as intended. If performance becomes a concern or additional features are needed, further optimizations can be made based on those requirements.