Browse Source

pref: 优化内容

jiaxing.liao 1 month ago
parent
commit
88c5efe880

+ 89 - 3
apps/web/src/nodes/_base/CodeEditor.vue

@@ -63,7 +63,14 @@ const { t } = useI18n()
 
 let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
 let model: editor.ITextModel | null = null
+let layoutFrame = 0
 const editContainer = ref<HTMLElement>()
+const bodyHeight = ref(props.height)
+const resizeState = reactive({
+	startY: 0,
+	startHeight: props.height,
+	isDragging: false
+})
 
 const isFullScreen = ref(false)
 const { monacoTheme, themeClass, toggleTheme } = useLocalEditorTheme({
@@ -154,6 +161,18 @@ watch(isFullScreen, () => {
 	})
 })
 
+watch(
+	() => props.height,
+	(height) => {
+		if (bodyHeight.value < height) {
+			bodyHeight.value = height
+		}
+		nextTick(() => {
+			monacoEditor?.layout()
+		})
+	}
+)
+
 /**
  * @description: 发送回调(formatValue:json,需要转换处理)
  * @return {*}
@@ -203,7 +222,7 @@ const fullScreenStyle = computed(() => {
 	if (isFullScreen.value) return {}
 
 	return {
-		height: `${props.height + 40}px`
+		height: `${bodyHeight.value + 40}px`
 	}
 })
 /**
@@ -217,6 +236,45 @@ const onToggleTheme = () => {
 	toggleTheme()
 }
 
+const syncEditorLayout = () => {
+	if (layoutFrame) {
+		cancelAnimationFrame(layoutFrame)
+	}
+	layoutFrame = requestAnimationFrame(() => {
+		layoutFrame = 0
+		monacoEditor?.layout()
+	})
+}
+
+const stopResize = () => {
+	if (!resizeState.isDragging) return
+	resizeState.isDragging = false
+	document.body.style.userSelect = ''
+	document.body.style.cursor = ''
+	window.removeEventListener('mousemove', onResize)
+	window.removeEventListener('mouseup', stopResize)
+}
+
+const onResize = (event: MouseEvent) => {
+	if (!resizeState.isDragging || isFullScreen.value) return
+	const nextHeight = resizeState.startHeight + event.clientY - resizeState.startY
+	const targetHeight = Math.max(props.height, nextHeight)
+	if (targetHeight === bodyHeight.value) return
+	bodyHeight.value = targetHeight
+	syncEditorLayout()
+}
+
+const onResizeStart = (event: MouseEvent) => {
+	if (isFullScreen.value) return
+	resizeState.startY = event.clientY
+	resizeState.startHeight = bodyHeight.value
+	resizeState.isDragging = true
+	document.body.style.userSelect = 'none'
+	document.body.style.cursor = 'ns-resize'
+	window.addEventListener('mousemove', onResize)
+	window.addEventListener('mouseup', stopResize)
+}
+
 /**
  * 设置文本
  * @param text
@@ -259,6 +317,10 @@ function insertText(text: string) {
 
 // 销毁model
 onBeforeUnmount(() => {
+	stopResize()
+	if (layoutFrame) {
+		cancelAnimationFrame(layoutFrame)
+	}
 	monacoEditor?.dispose()
 	model?.dispose()
 })
@@ -272,7 +334,10 @@ defineExpose({
 	<Teleport :disabled="!appendTo" :to="appendTo">
 		<div
 			class="monacoEditor !m-0"
-			:class="[themeClass, { 'is-fullscreen': isFullScreen }]"
+			:class="[
+				themeClass,
+				{ 'is-fullscreen': isFullScreen, 'is-resizing': resizeState.isDragging }
+			]"
 			:style="fullScreenStyle"
 		>
 			<div class="tools h-[33px] flex items-center justify-between gap-2" v-if="props.tools">
@@ -347,6 +412,7 @@ defineExpose({
 			<!-- 编辑器 -->
 			<div class="editor-wrapper">
 				<div ref="editContainer" class="code-editor"></div>
+				<div v-if="!isFullScreen" class="resize-handle" @mousedown.prevent="onResizeStart"></div>
 			</div>
 		</div>
 	</Teleport>
@@ -382,7 +448,9 @@ defineExpose({
 </style>
 <style lang="less" scoped>
 .monacoEditor {
-	border: 1px solid #eee;
+	border: 1px solid #dcdfe6;
+	border-radius: 4px;
+	overflow: hidden;
 	display: flex;
 	flex-direction: column;
 	transition:
@@ -390,6 +458,12 @@ defineExpose({
 		inset 0.25s ease,
 		background-color 0.2s;
 
+	&.is-resizing {
+		transition:
+			inset 0.25s ease,
+			background-color 0.2s;
+	}
+
 	&.is-fullscreen {
 		position: absolute;
 		inset: 0;
@@ -414,6 +488,18 @@ defineExpose({
 	.editor-wrapper {
 		flex: 1;
 		overflow: hidden;
+		position: relative;
+		.resize-handle {
+			position: absolute;
+			bottom: 8px;
+			left: 50%;
+			transform: translateX(-50%);
+			width: 18px;
+			height: 6px;
+			background-color: #d0d5dd;
+			border-radius: 8px;
+			cursor: ns-resize;
+		}
 	}
 
 	.code-editor {

+ 10 - 3
apps/web/src/nodes/src/question-classifier/setter.vue

@@ -96,6 +96,7 @@ interface Emits {
 
 const props = defineProps<{
 	data: QuestionClassifierData
+	id: string
 }>()
 
 const emit = defineEmits<Emits>()
@@ -115,8 +116,14 @@ const texts = computed(() => ({
 
 const formData = useSetterModel<QuestionClassifierData>(props, emit)
 
-const createClassItem = (index: number): ClassItem => ({
-	id: `class-${Date.now()}-${index}`,
+const getId = () => {
+	const ids = formData.value.classes.map((c) => c.id.split('_')[1]).map(Number)
+	const maxId = Math.max(...ids, 0)
+	return props.id + '_' + (maxId + 1)
+}
+
+const createClassItem = (index?: number): ClassItem => ({
+	id: index ? `${props.id}_${index}` : getId(),
 	name: '',
 	instruction: ''
 })
@@ -133,7 +140,7 @@ const handleAddClass = () => {
 	if (!formData.value.classes) {
 		formData.value.classes = []
 	}
-	formData.value.classes.push(createClassItem(formData.value.classes.length + 1))
+	formData.value.classes.push(createClassItem())
 }
 
 const handleRemoveClass = (index: number) => {

+ 54 - 12
apps/web/src/views/editor/NodeView.vue

@@ -150,6 +150,7 @@ const pendingNodeStatusTimers = new Map<string, number>()
 const pendingEdges = ref<
 	Array<Connection & { id: string; type?: string; data?: Record<string, unknown> }>
 >([])
+const pendingNodes = ref<IWorkflowNode[]>([])
 
 const removeNodeLibaryPopoverAnchor = () => {
 	nodeLibaryPopoverAnchorRef.value?.remove()
@@ -278,9 +279,13 @@ const workflowWithExecutionState = computed(() => {
 			}
 		}
 	})
+	const stableNodeIds = new Set((baseWorkflow.nodes || []).map((node) => node.id))
+	const displayPendingNodes = pendingNodes.value.filter((node) => !stableNodeIds.has(node.id))
 
 	const stableEdgeKeys = new Set(
-		(baseWorkflow.edges || []).map((edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}`)
+		(baseWorkflow.edges || []).map(
+			(edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}`
+		)
 	)
 	const displayPendingEdges = pendingEdges.value.filter(
 		(edge) => !stableEdgeKeys.has(`${edge.source}->${edge.target}->${edge.sourceHandle || ''}`)
@@ -288,7 +293,7 @@ const workflowWithExecutionState = computed(() => {
 
 	return {
 		...baseWorkflow,
-		nodes,
+		nodes: [...nodes, ...displayPendingNodes],
 		edges: [...(baseWorkflow.edges || []), ...displayPendingEdges]
 	} as IWorkflow
 })
@@ -626,23 +631,57 @@ const handleNodeCreate = (value: { type: string; position?: XYPosition } | strin
 				edgeId: connection.id,
 				newNode: newNodeParam
 			}
-			agent.postAgentDoNewAgentNodeWithEdge(params).then(async (response) => {
-				if (
-					handleApiResult(
-						response,
-						t('pages.nodeView.messages.nodeAdded'),
-						t('pages.nodeView.messages.addNodeFailed')
-					)
-				) {
-					await props.reloadWorkflow(props.workflow.id)
+			const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
+			pendingNodes.value.push({
+				...newNodeParam,
+				id: pendingNodeId,
+				name: 'pendding...',
+				type: 'canvas-node',
+				selected: false,
+				data: {
+					...(newNodeParam.data || {}),
+					pending: true
 				}
-			})
+			} as IWorkflowNode)
+			agent
+				.postAgentDoNewAgentNodeWithEdge(params)
+				.then(async (response) => {
+					if (
+						handleApiResult(
+							response,
+							t('pages.nodeView.messages.nodeAdded'),
+							t('pages.nodeView.messages.addNodeFailed')
+						)
+					) {
+						await props.reloadWorkflow(props.workflow.id)
+					}
+				})
+				.catch((error) => {
+					console.error('postAgentDoNewAgentNodeWithEdge error', error)
+					ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
+				})
+				.finally(() => {
+					pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
+				})
 			onHideNodeLibary()
 			return
 		}
 
 		onHideNodeLibary()
 
+		const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
+		pendingNodes.value.push({
+			...newNodeParam,
+			id: pendingNodeId,
+			name: 'pendding...',
+			type: 'canvas-node',
+			selected: false,
+			data: {
+				...(newNodeParam.data || {}),
+				pending: true
+			}
+		} as IWorkflowNode)
+
 		agent
 			.postAgentDoNewAgentNode(newNodeParam)
 			.then(async (response) => {
@@ -660,6 +699,9 @@ const handleNodeCreate = (value: { type: string; position?: XYPosition } | strin
 				console.error('postDoNewAgentNode error', error)
 				ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
 			})
+			.finally(() => {
+				pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
+			})
 	}
 }
 

+ 1 - 0
packages/workflow/src/components/Canvas.vue

@@ -424,6 +424,7 @@ async function onNodesInitialized() {
 				maxZoom: 1
 			})
 		} else {
+			console.log('defaultViewport', defaultViewport.value)
 			await setViewport(defaultViewport.value)
 		}
 		loaded = true

+ 2 - 2
packages/workflow/src/components/elements/edges/CanvasEdge.vue

@@ -101,7 +101,7 @@ const onDelete = () => {
 			}"
 			class="nodrag nopan"
 		>
-			<div v-if="isPendingEdge" class="text-sm text-gray-500">loading...</div>
+			<div v-if="isPendingEdge" class="text-sm text-gray-500">pendding...</div>
 			<div v-if="renderToolbar && !isPendingEdge" class="flex">
 				<IconButton :edge-add-btn="id" icon="lucide:plus" size="small" square @click="onAdd" />
 				<IconButton icon="lucide:brush-cleaning" size="small" square @click="onDelete" />
@@ -122,6 +122,6 @@ const onDelete = () => {
 
 .canvas-edge-path.is-pending {
 	stroke-dasharray: 8 6;
-	animation: edge-loading-dash 900ms linear infinite;
+	animation: edge-loading-dash 3.6s linear infinite;
 }
 </style>

+ 52 - 2
packages/workflow/src/components/elements/nodes/CanvasNode.vue

@@ -151,6 +151,7 @@ const outputs = computed(() => {
 })
 
 const hideToolBar = computed(() => nodeMap.value?.[props.node?.data.nodeType]?.hideToolBar)
+const isPendingNode = computed(() => !!props.node?.data?.pending)
 
 const onUpdate = (prop: Record<string, unknown>) => {
 	emit('update', props.id, prop)
@@ -201,11 +202,19 @@ provideCanvasNodeContext({
 			@loop:child:click:connection:add="emit('loop:child:click:connection:add', $event)"
 		/>
 
-		<template v-for="target in inputs" :key="'handle-inputs-port' + target.index">
+		<template
+			v-if="!isPendingNode"
+			v-for="target in inputs"
+			:key="'handle-inputs-port' + target.index"
+		>
 			<CanvasHandle v-bind="target" type="target" />
 		</template>
 
-		<template v-for="source in outputs" :key="'handle-outputs-port' + source.index">
+		<template
+			v-if="!isPendingNode"
+			v-for="source in outputs"
+			:key="'handle-outputs-port' + source.index"
+		>
 			<CanvasHandle
 				v-bind="source"
 				:node-id="props.id"
@@ -280,6 +289,32 @@ provideCanvasNodeContext({
 	border-color: #f59e0b !important;
 }
 
+.canvas-node-status-shell[data-pending='true'] {
+	opacity: 0.7;
+	border-color: #409eff !important;
+	filter: none !important;
+}
+
+.canvas-node-status-shell[data-pending='true']::before {
+	content: '';
+	position: absolute;
+	inset: 0;
+	border-radius: inherit;
+	padding: 1.5px;
+	background: repeating-conic-gradient(
+		from var(--pending-dash-angle),
+		rgba(64, 158, 255, 0.95) 0deg 10deg,
+		transparent 10deg 20deg
+	);
+	-webkit-mask:
+		linear-gradient(#000 0 0) content-box,
+		linear-gradient(#000 0 0);
+	-webkit-mask-composite: xor;
+	mask-composite: exclude;
+	pointer-events: none;
+	animation: canvas-node-pending-dash-flow 8s linear infinite;
+}
+
 @keyframes canvas-node-status-pulse {
 	0%,
 	100% {
@@ -317,4 +352,19 @@ provideCanvasNodeContext({
 		opacity: 0.7;
 	}
 }
+
+@property --pending-dash-angle {
+	syntax: '<angle>';
+	inherits: false;
+	initial-value: 0deg;
+}
+
+@keyframes canvas-node-pending-dash-flow {
+	from {
+		--pending-dash-angle: 0deg;
+	}
+	to {
+		--pending-dash-angle: 360deg;
+	}
+}
 </style>

+ 1 - 0
packages/workflow/src/components/elements/nodes/render-types/NodeDefault.vue

@@ -46,6 +46,7 @@ const warningInfo = computed(() => {
 <template>
 	<div
 		:data-execution-status="executionStatus"
+		:data-pending="nodeData?.data?.pending ? 'true' : 'false'"
 		:class="nodeClass"
 		class="canvas-node-status-shell default-node w-full h-full bg-#fff box-border border-1.5px border-solid border-#dcdcdc rounded-8px relative"
 	>

+ 1 - 0
packages/workflow/src/components/elements/nodes/render-types/NodeIcon.vue

@@ -27,6 +27,7 @@ const executionStatus = computed(() => getNodeExecutionStatus(node.props.value.n
 	<el-tooltip :content="nodeType?.displayName || '节点标题'">
 		<div
 			:data-execution-status="executionStatus"
+			:data-pending="nodeData?.pending ? 'true' : 'false'"
 			:class="nodeClass"
 			class="canvas-node-status-shell bg-#fff rounded-8px relative border-1.5px border-solid border-transparent"
 		>

+ 1 - 0
packages/workflow/src/components/elements/nodes/render-types/NodeLoop.vue

@@ -116,6 +116,7 @@ function onAddNode() {
 
 	<div
 		:data-execution-status="executionStatus"
+		:data-pending="nodeData?.pending ? 'true' : 'false'"
 		:class="nodeClass"
 		class="canvas-node-status-shell w-full h-full box-border node-loop rounded-12px border-2 border-solid border-#dcdcdc bg-#fff overflow-hidden flex flex-col"
 	>