mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-07-27 16:37:58 +00:00
Merge branch 'OpenBMB:main' into feature/add-loop-timer
This commit is contained in:
commit
913ff43104
351
frontend/src/components/RichTooltip.vue
Normal file
351
frontend/src/components/RichTooltip.vue
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="wrapperRef"
|
||||||
|
class="tooltip-wrapper"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@keydown.space.prevent="handleKeyboardActivate"
|
||||||
|
@keydown.enter.prevent="handleKeyboardActivate"
|
||||||
|
@keydown.escape="handleDismiss"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
|
||||||
|
<teleport to="body">
|
||||||
|
<transition name="tooltip-fade">
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
ref="tooltipRef"
|
||||||
|
class="rich-tooltip"
|
||||||
|
:class="[`tooltip-${placement}`, { 'tooltip-keyboard-active': keyboardActive }]"
|
||||||
|
:style="tooltipStyle"
|
||||||
|
role="tooltip"
|
||||||
|
:aria-hidden="!isVisible"
|
||||||
|
@mouseenter="handleTooltipMouseEnter"
|
||||||
|
@mouseleave="handleTooltipMouseLeave"
|
||||||
|
>
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<h4 v-if="content.title" class="tooltip-title">{{ content.title }}</h4>
|
||||||
|
<p class="tooltip-description">{{ content.description }}</p>
|
||||||
|
<ul v-if="content.examples && content.examples.length" class="tooltip-examples">
|
||||||
|
<li v-for="(example, index) in content.examples" :key="index">{{ example }}</li>
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
v-if="content.learnMoreUrl"
|
||||||
|
:href="content.learnMoreUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="tooltip-learn-more"
|
||||||
|
@click="handleLearnMore"
|
||||||
|
>
|
||||||
|
Learn More →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" :data-placement="placement"></div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (value) => {
|
||||||
|
return value && typeof value.description === 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delay: {
|
||||||
|
type: Number,
|
||||||
|
default: 500
|
||||||
|
},
|
||||||
|
placement: {
|
||||||
|
type: String,
|
||||||
|
default: 'top',
|
||||||
|
validator: (value) => ['top', 'bottom', 'left', 'right'].includes(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const tooltipRef = ref(null)
|
||||||
|
const wrapperRef = ref(null)
|
||||||
|
const tooltipStyle = ref({})
|
||||||
|
const hoverTimeout = ref(null)
|
||||||
|
const mouseInTooltip = ref(false)
|
||||||
|
const keyboardActive = ref(false)
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
keyboardActive.value = false
|
||||||
|
clearTimeout(hoverTimeout.value)
|
||||||
|
hoverTimeout.value = setTimeout(() => {
|
||||||
|
showTooltip()
|
||||||
|
}, props.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
clearTimeout(hoverTimeout.value)
|
||||||
|
// Delay hiding to allow moving into tooltip
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!mouseInTooltip.value) {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
// Don't auto-show on focus, wait for keyboard activation
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (keyboardActive.value) {
|
||||||
|
hideTooltip()
|
||||||
|
keyboardActive.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyboardActivate = () => {
|
||||||
|
keyboardActive.value = true
|
||||||
|
showTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
hideTooltip()
|
||||||
|
keyboardActive.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTooltipMouseEnter = () => {
|
||||||
|
mouseInTooltip.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTooltipMouseLeave = () => {
|
||||||
|
mouseInTooltip.value = false
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLearnMore = () => {
|
||||||
|
// Keep tooltip open when clicking learn more
|
||||||
|
// The link will open in new tab
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTooltip = async () => {
|
||||||
|
isVisible.value = true
|
||||||
|
await nextTick()
|
||||||
|
calculatePosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideTooltip = () => {
|
||||||
|
isVisible.value = false
|
||||||
|
tooltipStyle.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatePosition = () => {
|
||||||
|
if (!tooltipRef.value || !wrapperRef.value) return
|
||||||
|
|
||||||
|
const wrapper = wrapperRef.value.getBoundingClientRect()
|
||||||
|
const tooltip = tooltipRef.value.getBoundingClientRect()
|
||||||
|
const viewport = {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = 12 // Distance from target element
|
||||||
|
let top = 0
|
||||||
|
let left = 0
|
||||||
|
let actualPlacement = props.placement
|
||||||
|
|
||||||
|
// Calculate initial position based on preferred placement
|
||||||
|
switch (props.placement) {
|
||||||
|
case 'top':
|
||||||
|
top = wrapper.top - tooltip.height - offset
|
||||||
|
left = wrapper.left + (wrapper.width / 2) - (tooltip.width / 2)
|
||||||
|
break
|
||||||
|
case 'bottom':
|
||||||
|
top = wrapper.bottom + offset
|
||||||
|
left = wrapper.left + (wrapper.width / 2) - (tooltip.width / 2)
|
||||||
|
break
|
||||||
|
case 'left':
|
||||||
|
top = wrapper.top + (wrapper.height / 2) - (tooltip.height / 2)
|
||||||
|
left = wrapper.left - tooltip.width - offset
|
||||||
|
break
|
||||||
|
case 'right':
|
||||||
|
top = wrapper.top + (wrapper.height / 2) - (tooltip.height / 2)
|
||||||
|
left = wrapper.right + offset
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust if tooltip would go off-screen
|
||||||
|
if (left < 10) {
|
||||||
|
left = 10
|
||||||
|
} else if (left + tooltip.width > viewport.width - 10) {
|
||||||
|
left = viewport.width - tooltip.width - 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top < 10) {
|
||||||
|
// If it would go above viewport, flip to bottom
|
||||||
|
if (props.placement === 'top') {
|
||||||
|
top = wrapper.bottom + offset
|
||||||
|
actualPlacement = 'bottom'
|
||||||
|
} else {
|
||||||
|
top = 10
|
||||||
|
}
|
||||||
|
} else if (top + tooltip.height > viewport.height - 10) {
|
||||||
|
// If it would go below viewport, flip to top
|
||||||
|
if (props.placement === 'bottom') {
|
||||||
|
top = wrapper.top - tooltip.height - offset
|
||||||
|
actualPlacement = 'top'
|
||||||
|
} else {
|
||||||
|
top = viewport.height - tooltip.height - 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipStyle.value = {
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to scroll and zoom events to dismiss tooltip
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (isVisible.value && !keyboardActive.value) {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isVisible.value) {
|
||||||
|
calculatePosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// wrapperRef is now set via template ref, no need to query for it
|
||||||
|
window.addEventListener('scroll', handleScroll, true)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearTimeout(hoverTimeout.value)
|
||||||
|
window.removeEventListener('scroll', handleScroll, true)
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tooltip-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 320px;
|
||||||
|
background: rgba(26, 26, 26, 0.95);
|
||||||
|
color: #f2f2f2;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-keyboard-active {
|
||||||
|
outline: 2px solid #60b7ff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-description {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-examples {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: #c0c0c0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-examples li {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-learn-more {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #60b7ff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-learn-more:hover {
|
||||||
|
color: #99ffeb;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: inherit;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-top .tooltip-arrow {
|
||||||
|
bottom: -5px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-bottom .tooltip-arrow {
|
||||||
|
top: -5px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-left .tooltip-arrow {
|
||||||
|
right: -5px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-right .tooltip-arrow {
|
||||||
|
left: -5px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-fade-enter-active,
|
||||||
|
.tooltip-fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -21,6 +21,13 @@
|
|||||||
</label>
|
</label>
|
||||||
<p class="setting-desc">Automatically expand message content in the chat view.</p>
|
<p class="setting-desc">Automatically expand message content in the chat view.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" v-model="localConfig.ENABLE_HELP_TOOLTIPS">
|
||||||
|
Enable help tooltips
|
||||||
|
</label>
|
||||||
|
<p class="setting-desc">Show contextual help tooltips throughout the workflow interface.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="cancel-button" @click="close">Cancel</button>
|
<button class="cancel-button" @click="close">Cancel</button>
|
||||||
@ -44,7 +51,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const localConfig = reactive({
|
const localConfig = reactive({
|
||||||
AUTO_SHOW_ADVANCED: false,
|
AUTO_SHOW_ADVANCED: false,
|
||||||
AUTO_EXPAND_MESSAGES: false
|
AUTO_EXPAND_MESSAGES: false,
|
||||||
|
ENABLE_HELP_TOOLTIPS: true
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.isVisible, (newVal) => {
|
watch(() => props.isVisible, (newVal) => {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Handle, Position } from '@vue-flow/core'
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
import RichTooltip from './RichTooltip.vue'
|
||||||
|
import { helpContent } from '../utils/helpContent.js'
|
||||||
|
import { configStore } from '../utils/configStore.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
@ -12,10 +15,19 @@ const props = defineProps({
|
|||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shouldShowTooltip = computed(() => configStore.ENABLE_HELP_TOOLTIPS)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="start-node" :style="{ opacity: data.opacity ?? 1 }">
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.startNode" placement="right">
|
||||||
|
<div class="start-node" :style="{ opacity: data.opacity ?? 1 }">
|
||||||
|
<div class="start-node-bubble" title="Start Node"></div>
|
||||||
|
<!-- Provide source handle at right -->
|
||||||
|
<Handle id="source" type="source" :position="Position.Right" class="start-node-handle" />
|
||||||
|
</div>
|
||||||
|
</RichTooltip>
|
||||||
|
<div v-else class="start-node" :style="{ opacity: data.opacity ?? 1 }">
|
||||||
<div class="start-node-bubble" title="Start Node"></div>
|
<div class="start-node-bubble" title="Start Node"></div>
|
||||||
<!-- Provide source handle at right -->
|
<!-- Provide source handle at right -->
|
||||||
<Handle id="source" type="source" :position="Position.Right" class="start-node-handle" />
|
<Handle id="source" type="source" :position="Position.Right" class="start-node-handle" />
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
import { computed, ref, nextTick, watch } from 'vue'
|
import { computed, ref, nextTick, watch } from 'vue'
|
||||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath, getSmoothStepPath, MarkerType } from '@vue-flow/core'
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath, getSmoothStepPath, MarkerType } from '@vue-flow/core'
|
||||||
import { useVueFlow } from '@vue-flow/core'
|
import { useVueFlow } from '@vue-flow/core'
|
||||||
|
import RichTooltip from './RichTooltip.vue'
|
||||||
|
import { getEdgeHelp } from '../utils/helpContent.js'
|
||||||
|
import { configStore } from '../utils/configStore.js'
|
||||||
|
|
||||||
const { findNode } = useVueFlow()
|
const { findNode } = useVueFlow()
|
||||||
|
|
||||||
@ -562,6 +565,44 @@ const labelStyle = computed(() => {
|
|||||||
|
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const edgeHelpContent = computed(() => getEdgeHelp(props.data))
|
||||||
|
|
||||||
|
// Compute tooltip position (geometric midpoint of edge path)
|
||||||
|
const tooltipX = computed(() => {
|
||||||
|
const sourceNode = findNode(props.source)
|
||||||
|
const targetNode = findNode(props.target)
|
||||||
|
const isSelfLoop = props.source === props.target
|
||||||
|
const isLeftwardEdge = props.targetX < props.sourceX
|
||||||
|
|
||||||
|
// For self-loops and leftward edges with arc paths, use arc midpoint calculation
|
||||||
|
if (isSelfLoop || isLeftwardEdge) {
|
||||||
|
// Return labelX for arcs (already computed as arc midpoint)
|
||||||
|
return labelX.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// For bezier paths, compute midpoint at t=0.5
|
||||||
|
return (props.sourceX + props.targetX) / 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const tooltipY = computed(() => {
|
||||||
|
const sourceNode = findNode(props.source)
|
||||||
|
const targetNode = findNode(props.target)
|
||||||
|
const isSelfLoop = props.source === props.target
|
||||||
|
const isLeftwardEdge = props.targetX < props.sourceX
|
||||||
|
|
||||||
|
// For self-loops and leftward edges with arc paths, use arc midpoint calculation
|
||||||
|
if (isSelfLoop || isLeftwardEdge) {
|
||||||
|
// Return labelY for arcs (already computed as arc midpoint)
|
||||||
|
return labelY.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// For bezier paths, compute midpoint at t=0.5
|
||||||
|
return (props.sourceY + props.targetY) / 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const shouldShowTooltip = computed(() => configStore.ENABLE_HELP_TOOLTIPS)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -614,6 +655,23 @@ const labelStyle = computed(() => {
|
|||||||
:animated="false"
|
:animated="false"
|
||||||
class="nodrag nopan"
|
class="nodrag nopan"
|
||||||
/>
|
/>
|
||||||
|
<!-- Tooltip-enabled hover area at edge midpoint -->
|
||||||
|
<EdgeLabelRenderer v-if="shouldShowTooltip">
|
||||||
|
<RichTooltip :content="edgeHelpContent" placement="top">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${tooltipX}px, ${tooltipY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}"
|
||||||
|
class="edge-tooltip-trigger"
|
||||||
|
/>
|
||||||
|
</RichTooltip>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
<EdgeLabelRenderer v-if="edgeLabel">
|
<EdgeLabelRenderer v-if="edgeLabel">
|
||||||
<div
|
<div
|
||||||
:key="edgeLabelKey"
|
:key="edgeLabelKey"
|
||||||
@ -649,4 +707,13 @@ const labelStyle = computed(() => {
|
|||||||
.animated-label {
|
.animated-label {
|
||||||
animation: label-pulse var(--label-anim-duration) infinite linear;
|
animation: label-pulse var(--label-anim-duration) infinite linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edge-tooltip-trigger {
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-tooltip-trigger:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
|||||||
import { Handle, Position } from '@vue-flow/core'
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
import { getNodeStyles } from '../utils/colorUtils.js'
|
import { getNodeStyles } from '../utils/colorUtils.js'
|
||||||
import { spriteFetcher } from '../utils/spriteFetcher.js'
|
import { spriteFetcher } from '../utils/spriteFetcher.js'
|
||||||
|
import RichTooltip from './RichTooltip.vue'
|
||||||
|
import { getNodeHelp } from '../utils/helpContent.js'
|
||||||
|
import { configStore } from '../utils/configStore.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
@ -37,6 +40,10 @@ const nodeDescription = computed(() => props.data?.description || '')
|
|||||||
const isActive = computed(() => props.isActive)
|
const isActive = computed(() => props.isActive)
|
||||||
const dynamicStyles = computed(() => getNodeStyles(nodeType.value))
|
const dynamicStyles = computed(() => getNodeStyles(nodeType.value))
|
||||||
|
|
||||||
|
const nodeHelpContent = computed(() => getNodeHelp(nodeType.value))
|
||||||
|
|
||||||
|
const shouldShowTooltip = computed(() => configStore.ENABLE_HELP_TOOLTIPS && nodeHelpContent.value)
|
||||||
|
|
||||||
// Compute the current sprite path based on active state and walking frame
|
// Compute the current sprite path based on active state and walking frame
|
||||||
const currentSprite = computed(() => {
|
const currentSprite = computed(() => {
|
||||||
if (!props.sprite) return ''
|
if (!props.sprite) return ''
|
||||||
@ -83,7 +90,43 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="workflow-node-container">
|
<RichTooltip v-if="shouldShowTooltip" :content="nodeHelpContent" placement="top">
|
||||||
|
<div class="workflow-node-container">
|
||||||
|
<div v-if="props.sprite" class="workflow-node-sprite">
|
||||||
|
<img :src="currentSprite" :alt="`${nodeId} sprite`" class="node-sprite-image" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="workflow-node"
|
||||||
|
:class="{ 'workflow-node-active': isActive }"
|
||||||
|
:data-type="nodeType"
|
||||||
|
:style="dynamicStyles"
|
||||||
|
@mouseenter="$emit('hover', nodeId)"
|
||||||
|
@mouseleave="$emit('leave', nodeId)"
|
||||||
|
>
|
||||||
|
<div class="workflow-node-header">
|
||||||
|
<span class="workflow-node-type">{{ nodeType }}</span>
|
||||||
|
<span class="workflow-node-id">{{ nodeId }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="nodeDescription" class="workflow-node-description">
|
||||||
|
{{ nodeDescription }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
id="source"
|
||||||
|
type="source"
|
||||||
|
:position="Position.Right"
|
||||||
|
class="workflow-node-handle"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
id="target"
|
||||||
|
type="target"
|
||||||
|
:position="Position.Left"
|
||||||
|
class="workflow-node-handle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RichTooltip>
|
||||||
|
<div v-else class="workflow-node-container">
|
||||||
<div v-if="props.sprite" class="workflow-node-sprite">
|
<div v-if="props.sprite" class="workflow-node-sprite">
|
||||||
<img :src="currentSprite" :alt="`${nodeId} sprite`" class="node-sprite-image" />
|
<img :src="currentSprite" :alt="`${nodeId} sprite`" class="node-sprite-image" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import markdownItAnchor from 'markdown-it-anchor'
|
import markdownItAnchor from 'markdown-it-anchor'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const renderedContent = ref('')
|
const renderedContent = ref('')
|
||||||
const currentLang = ref('en') // 'zh' for Chinese, 'en' for English
|
const currentLang = ref('en') // 'zh' for Chinese, 'en' for English
|
||||||
const markdownBody = ref(null)
|
const markdownBody = ref(null)
|
||||||
@ -28,6 +31,23 @@ md.use(markdownItAnchor, {
|
|||||||
|
|
||||||
const getTutorialFile = () => (currentLang.value === 'en' ? '/tutorial-en.md' : '/tutorial-zh.md')
|
const getTutorialFile = () => (currentLang.value === 'en' ? '/tutorial-en.md' : '/tutorial-zh.md')
|
||||||
|
|
||||||
|
const scrollToHash = () => {
|
||||||
|
// Wait for DOM to update, then scroll to hash if present
|
||||||
|
nextTick(() => {
|
||||||
|
if (route.hash) {
|
||||||
|
// Remove the '#' from the hash
|
||||||
|
const targetId = route.hash.slice(1)
|
||||||
|
const targetElement = document.getElementById(targetId)
|
||||||
|
|
||||||
|
if (targetElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const addCopyButtons = () => {
|
const addCopyButtons = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const container = markdownBody.value
|
const container = markdownBody.value
|
||||||
@ -71,6 +91,7 @@ const loadTutorial = async () => {
|
|||||||
|
|
||||||
renderedContent.value = md.render(text)
|
renderedContent.value = md.render(text)
|
||||||
addCopyButtons()
|
addCopyButtons()
|
||||||
|
scrollToHash()
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to fetch tutorial markdown')
|
console.error('Failed to fetch tutorial markdown')
|
||||||
}
|
}
|
||||||
@ -117,6 +138,7 @@ onMounted(() => {
|
|||||||
box-shadow: 0 4px 32px 0 rgba(0, 255, 255, 0.08), 0 0 0 2px #00eaff33;
|
box-shadow: 0 4px 32px 0 rgba(0, 255, 255, 0.08), 0 0 0 2px #00eaff33;
|
||||||
border: 1.5px solid #00eaff33;
|
border: 1.5px solid #00eaff33;
|
||||||
transition: box-shadow 0.3s;
|
transition: box-shadow 0.3s;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-switch {
|
.lang-switch {
|
||||||
@ -175,6 +197,23 @@ onMounted(() => {
|
|||||||
color: #00eaff;
|
color: #00eaff;
|
||||||
text-shadow: 0 0 8px #00eaff44;
|
text-shadow: 0 0 8px #00eaff44;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
|
scroll-margin-top: 20px; /* Offset for hash scroll target */
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight targeted heading */
|
||||||
|
:deep(.markdown-body h1:target),
|
||||||
|
:deep(.markdown-body h2:target),
|
||||||
|
:deep(.markdown-body h3:target),
|
||||||
|
:deep(.markdown-body h4:target),
|
||||||
|
:deep(.markdown-body h5:target),
|
||||||
|
:deep(.markdown-body h6:target) {
|
||||||
|
background: rgba(0, 234, 255, 0.15);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 16px rgba(0, 234, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.markdown-body h1) { font-size: 2.2em; border-bottom: 1px solid #00eaff33; padding-bottom: 0.3em; }
|
:deep(.markdown-body h1) { font-size: 2.2em; border-bottom: 1px solid #00eaff33; padding-bottom: 0.3em; }
|
||||||
|
|||||||
@ -87,7 +87,16 @@
|
|||||||
>
|
>
|
||||||
<!-- Pane context menu -->
|
<!-- Pane context menu -->
|
||||||
<template v-if="contextMenuType === 'pane'">
|
<template v-if="contextMenuType === 'pane'">
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createNode" placement="right">
|
||||||
|
<div
|
||||||
|
class="context-menu-item"
|
||||||
|
@click.stop="() => { hideContextMenu(); openCreateNodeModal(); }"
|
||||||
|
>
|
||||||
|
Create Node
|
||||||
|
</div>
|
||||||
|
</RichTooltip>
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
@click.stop="() => { hideContextMenu(); openCreateNodeModal(); }"
|
@click.stop="() => { hideContextMenu(); openCreateNodeModal(); }"
|
||||||
>
|
>
|
||||||
@ -97,13 +106,31 @@
|
|||||||
|
|
||||||
<!-- Node context menu -->
|
<!-- Node context menu -->
|
||||||
<template v-else-if="contextMenuType === 'node'">
|
<template v-else-if="contextMenuType === 'node'">
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.copyNode" placement="right">
|
||||||
|
<div
|
||||||
|
class="context-menu-item"
|
||||||
|
@click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }"
|
||||||
|
>
|
||||||
|
Copy Node
|
||||||
|
</div>
|
||||||
|
</RichTooltip>
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
@click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }"
|
@click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }"
|
||||||
>
|
>
|
||||||
Copy Node
|
Copy Node
|
||||||
</div>
|
</div>
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.deleteNode" placement="right">
|
||||||
|
<div
|
||||||
|
class="context-menu-item"
|
||||||
|
@click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }"
|
||||||
|
>
|
||||||
|
Delete Node
|
||||||
|
</div>
|
||||||
|
</RichTooltip>
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
@click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }"
|
@click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }"
|
||||||
>
|
>
|
||||||
@ -113,7 +140,16 @@
|
|||||||
|
|
||||||
<!-- Edge context menu -->
|
<!-- Edge context menu -->
|
||||||
<template v-else-if="contextMenuType === 'edge'">
|
<template v-else-if="contextMenuType === 'edge'">
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.deleteEdge" placement="right">
|
||||||
|
<div
|
||||||
|
class="context-menu-item"
|
||||||
|
@click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }"
|
||||||
|
>
|
||||||
|
Delete Edge
|
||||||
|
</div>
|
||||||
|
</RichTooltip>
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
class="context-menu-item"
|
class="context-menu-item"
|
||||||
@click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }"
|
@click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }"
|
||||||
>
|
>
|
||||||
@ -141,13 +177,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeTab === 'graph'" class="editor-actions">
|
<div v-if="activeTab === 'graph'" class="editor-actions">
|
||||||
<button @click="openCreateNodeModal" class="glass-button">
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createNodeButton" placement="bottom">
|
||||||
|
<button @click="openCreateNodeModal" class="glass-button">
|
||||||
|
<span>Create Node</span>
|
||||||
|
</button>
|
||||||
|
</RichTooltip>
|
||||||
|
<button v-else @click="openCreateNodeModal" class="glass-button">
|
||||||
<span>Create Node</span>
|
<span>Create Node</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="openConfigureGraphModal" class="glass-button">
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.configureGraph" placement="bottom">
|
||||||
|
<button @click="openConfigureGraphModal" class="glass-button">
|
||||||
|
<span>Configure Graph</span>
|
||||||
|
</button>
|
||||||
|
</RichTooltip>
|
||||||
|
<button v-else @click="openConfigureGraphModal" class="glass-button">
|
||||||
<span>Configure Graph</span>
|
<span>Configure Graph</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="goToLaunch" class="launch-button-primary">
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.launch" placement="bottom">
|
||||||
|
<button @click="goToLaunch" class="launch-button-primary">
|
||||||
|
<span>Launch</span>
|
||||||
|
</button>
|
||||||
|
</RichTooltip>
|
||||||
|
<button v-else @click="goToLaunch" class="launch-button-primary">
|
||||||
<span>Launch</span>
|
<span>Launch</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -166,11 +217,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="showMenu" class="menu-dropdown">
|
<div v-if="showMenu" class="menu-dropdown">
|
||||||
<div @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div>
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.renameWorkflow" placement="left">
|
||||||
<div @click="openCopyWorkflowModal" class="menu-item">Copy Workflow</div>
|
<div @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div>
|
||||||
<div @click="openManageVarsModal" class="menu-item">Manage Variables</div>
|
</RichTooltip>
|
||||||
<div @click="openManageMemoriesModal" class="menu-item">Manage Memories</div>
|
<div v-else @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div>
|
||||||
<div @click="openCreateEdgeModal" class="menu-item">Create Edge</div>
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.copyWorkflow" placement="left">
|
||||||
|
<div @click="openCopyWorkflowModal" class="menu-item">Copy Workflow</div>
|
||||||
|
</RichTooltip>
|
||||||
|
<div v-else @click="openCopyWorkflowModal" class="menu-item">Copy Workflow</div>
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.manageVariables" placement="left">
|
||||||
|
<div @click="openManageVarsModal" class="menu-item">Manage Variables</div>
|
||||||
|
</RichTooltip>
|
||||||
|
<div v-else @click="openManageVarsModal" class="menu-item">Manage Variables</div>
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.manageMemories" placement="left">
|
||||||
|
<div @click="openManageMemoriesModal" class="menu-item">Manage Memories</div>
|
||||||
|
</RichTooltip>
|
||||||
|
<div v-else @click="openManageMemoriesModal" class="menu-item">Manage Memories</div>
|
||||||
|
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createEdge" placement="left">
|
||||||
|
<div @click="openCreateEdgeModal" class="menu-item">Create Edge</div>
|
||||||
|
</RichTooltip>
|
||||||
|
<div v-else @click="openCreateEdgeModal" class="menu-item">Create Edge</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@ -258,7 +324,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, watch, nextTick, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
|
import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
|
||||||
import { Background } from '@vue-flow/background'
|
import { Background } from '@vue-flow/background'
|
||||||
@ -270,8 +336,13 @@ import WorkflowNode from '../components/WorkflowNode.vue'
|
|||||||
import WorkflowEdge from '../components/WorkflowEdge.vue'
|
import WorkflowEdge from '../components/WorkflowEdge.vue'
|
||||||
import StartNode from '../components/StartNode.vue'
|
import StartNode from '../components/StartNode.vue'
|
||||||
import FormGenerator from '../components/FormGenerator.vue'
|
import FormGenerator from '../components/FormGenerator.vue'
|
||||||
|
import RichTooltip from '../components/RichTooltip.vue'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import { fetchYaml, fetchVueGraph, postVuegraphs, updateYaml, postYamlNameChange, postYamlCopy } from '../utils/apiFunctions'
|
import { fetchYaml, fetchVueGraph, postVuegraphs, updateYaml, postYamlNameChange, postYamlCopy } from '../utils/apiFunctions'
|
||||||
|
import { helpContent } from '../utils/helpContent.js'
|
||||||
|
import { configStore } from '../utils/configStore.js'
|
||||||
|
|
||||||
|
const shouldShowTooltip = computed(() => configStore.ENABLE_HELP_TOOLTIPS)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
workflowName: {
|
workflowName: {
|
||||||
|
|||||||
@ -25,7 +25,24 @@ const routes = [
|
|||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to.hash) {
|
||||||
|
return {
|
||||||
|
el: to.hash,
|
||||||
|
behavior: 'smooth',
|
||||||
|
// Add a small delay to ensure the element exists
|
||||||
|
top: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise scroll to top
|
||||||
|
return { top: 0 }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@ -4,7 +4,8 @@ const CONFIG_KEY = 'agent_config_settings'
|
|||||||
|
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
AUTO_SHOW_ADVANCED: false,
|
AUTO_SHOW_ADVANCED: false,
|
||||||
AUTO_EXPAND_MESSAGES: false
|
AUTO_EXPAND_MESSAGES: false,
|
||||||
|
ENABLE_HELP_TOOLTIPS: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize state from localStorage
|
// Initialize state from localStorage
|
||||||
|
|||||||
234
frontend/src/utils/helpContent.js
Normal file
234
frontend/src/utils/helpContent.js
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
export const helpContent = {
|
||||||
|
// Start Node Help
|
||||||
|
startNode: {
|
||||||
|
title: "Start Node",
|
||||||
|
description: "The entry point for your workflow. All nodes connected to the Start node will run in parallel when the workflow launches.",
|
||||||
|
examples: [
|
||||||
|
"Connect multiple nodes to start them simultaneously",
|
||||||
|
"The first nodes to execute receive your initial input"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#2-create-nodes"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Workflow Node Types
|
||||||
|
workflowNode: {
|
||||||
|
agent: {
|
||||||
|
title: "Agent Node",
|
||||||
|
description: "An AI agent that can reason, generate content, and use tools. Agents receive messages and produce responses based on their configuration.",
|
||||||
|
examples: [
|
||||||
|
"Content generation (writing, coding, analysis)",
|
||||||
|
"Decision making and routing",
|
||||||
|
"Tool usage (search, file operations, API calls)"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#agent-node"
|
||||||
|
},
|
||||||
|
human: {
|
||||||
|
title: "Human Node",
|
||||||
|
description: "Pauses workflow execution and waits for human input. Use this to review content, make decisions, or provide feedback.",
|
||||||
|
examples: [
|
||||||
|
"Review and approve generated content",
|
||||||
|
"Provide additional instructions or corrections",
|
||||||
|
"Choose between workflow paths"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#human-node"
|
||||||
|
},
|
||||||
|
python: {
|
||||||
|
title: "Python Node",
|
||||||
|
description: "Executes Python code on your local environment. The code runs in the workspace directory and can access uploaded files.",
|
||||||
|
examples: [
|
||||||
|
"Data processing and analysis",
|
||||||
|
"Running generated code",
|
||||||
|
"File manipulation"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#python-node"
|
||||||
|
},
|
||||||
|
passthrough: {
|
||||||
|
title: "Passthrough Node",
|
||||||
|
description: "Passes messages to the next node without modification. Useful for workflow organization and filtering outputs in loops.",
|
||||||
|
examples: [
|
||||||
|
"Preserve initial context in loops",
|
||||||
|
"Filter redundant outputs",
|
||||||
|
"Organize workflow structure"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#passthrough-node"
|
||||||
|
},
|
||||||
|
literal: {
|
||||||
|
title: "Literal Node",
|
||||||
|
description: "Outputs fixed text, ignoring all input. Use this to inject instructions or context at specific points in the workflow.",
|
||||||
|
examples: [
|
||||||
|
"Add fixed instructions before a node",
|
||||||
|
"Inject context or constraints",
|
||||||
|
"Provide test data"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#literal-node"
|
||||||
|
},
|
||||||
|
loop_counter: {
|
||||||
|
title: "Loop Counter Node",
|
||||||
|
description: "Limits loop iterations. Only produces output when the maximum count is reached, helping control infinite loops.",
|
||||||
|
examples: [
|
||||||
|
"Prevent runaway loops",
|
||||||
|
"Set maximum revision cycles",
|
||||||
|
"Control iterative processes"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#loop-counter-node"
|
||||||
|
},
|
||||||
|
subgraph: {
|
||||||
|
title: "Subgraph Node",
|
||||||
|
description: "Embeds another workflow as a reusable module. Enables modular design and workflow composition.",
|
||||||
|
examples: [
|
||||||
|
"Reuse common patterns across workflows",
|
||||||
|
"Break complex workflows into manageable pieces",
|
||||||
|
"Share workflows between teams"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#subgraph-node"
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
title: "Workflow Node",
|
||||||
|
description: "A node in your workflow. Click to view and edit its configuration.",
|
||||||
|
learnMoreUrl: "/tutorial#2-create-nodes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Workflow Edge Help
|
||||||
|
edge: {
|
||||||
|
basic: {
|
||||||
|
title: "Connection",
|
||||||
|
description: "Connects two nodes to control information flow and execution order. The upstream node's output becomes the downstream node's input.",
|
||||||
|
examples: [
|
||||||
|
"Data flows from source to target",
|
||||||
|
"Target executes after source completes"
|
||||||
|
],
|
||||||
|
learnMoreUrl: "/tutorial#what-is-an-edge"
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
enabled: {
|
||||||
|
description: "This connection triggers the downstream node to execute.",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
description: "This connection passes data but does NOT trigger execution. The downstream node only runs if triggered by another edge.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
hasCondition: {
|
||||||
|
description: "This connection has a condition. It only activates when the condition evaluates to true.",
|
||||||
|
learnMoreUrl: "/tutorial#edge-condition"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Context Menu Actions
|
||||||
|
contextMenu: {
|
||||||
|
createNode: {
|
||||||
|
description: "Create a new node in your workflow. Choose from Agent, Human, Python, and other node types.",
|
||||||
|
},
|
||||||
|
copyNode: {
|
||||||
|
description: "Duplicate this node with all its settings. The copy will have a blank ID that you must fill in.",
|
||||||
|
},
|
||||||
|
deleteNode: {
|
||||||
|
description: "Remove this node and all its connections from the workflow.",
|
||||||
|
},
|
||||||
|
deleteEdge: {
|
||||||
|
description: "Remove this connection between nodes.",
|
||||||
|
},
|
||||||
|
createNodeButton: {
|
||||||
|
description: "Open the node creation form. You can also right-click the canvas to create a node at a specific position.",
|
||||||
|
},
|
||||||
|
configureGraph: {
|
||||||
|
description: "Configure workflow-level settings like name, description, and global variables.",
|
||||||
|
},
|
||||||
|
launch: {
|
||||||
|
description: "Run your workflow with a task prompt. The workflow will execute and show you the results.",
|
||||||
|
},
|
||||||
|
createEdge: {
|
||||||
|
description: "Create a connection between nodes. You can also drag from a node's handle to create connections visually.",
|
||||||
|
},
|
||||||
|
manageVariables: {
|
||||||
|
description: "Define global variables (like API keys) that all nodes can access using ${VARIABLE_NAME} syntax.",
|
||||||
|
},
|
||||||
|
manageMemories: {
|
||||||
|
description: "Configure memory modules for long-term information storage and retrieval across workflow runs.",
|
||||||
|
},
|
||||||
|
renameWorkflow: {
|
||||||
|
description: "Change the name of this workflow file.",
|
||||||
|
},
|
||||||
|
copyWorkflow: {
|
||||||
|
description: "Create a duplicate of this entire workflow with a new name.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get help content by key path
|
||||||
|
* @param {string} key - Dot-separated path to content (e.g., 'workflowNode.agent')
|
||||||
|
* @returns {Object|null} Help content object or null if not found
|
||||||
|
*/
|
||||||
|
export function getHelpContent(key) {
|
||||||
|
const keys = key.split('.')
|
||||||
|
let content = helpContent
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (content && typeof content === 'object' && k in content) {
|
||||||
|
content = content[k]
|
||||||
|
} else {
|
||||||
|
// Return null for missing content instead of fallback
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we return an object with at least a description
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
return { description: content }
|
||||||
|
}
|
||||||
|
|
||||||
|
return content || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get node-specific help content based on node type
|
||||||
|
* @param {string} nodeType - The type of node (agent, human, python, etc.)
|
||||||
|
* @returns {Object|null} Help content for that node type, or null for unknown types
|
||||||
|
*/
|
||||||
|
export function getNodeHelp(nodeType) {
|
||||||
|
if (!nodeType) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = nodeType.toLowerCase()
|
||||||
|
const content = getHelpContent(`workflowNode.${type}`)
|
||||||
|
|
||||||
|
// Return null for unknown types (when content lookup fails)
|
||||||
|
// This prevents showing tooltips for custom/user-defined node types
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get edge help content based on edge properties
|
||||||
|
* @param {Object} edgeData - The edge data object
|
||||||
|
* @returns {Object} Combined help content for the edge
|
||||||
|
*/
|
||||||
|
export function getEdgeHelp(edgeData) {
|
||||||
|
const base = { ...helpContent.edge.basic }
|
||||||
|
|
||||||
|
// Add trigger information
|
||||||
|
const trigger = edgeData?.trigger !== undefined ? edgeData.trigger : true
|
||||||
|
if (!trigger) {
|
||||||
|
base.description += " " + helpContent.edge.trigger.disabled.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add condition information
|
||||||
|
if (edgeData?.condition) {
|
||||||
|
base.description += " " + helpContent.edge.condition.hasCondition.description
|
||||||
|
if (!base.learnMoreUrl) {
|
||||||
|
base.learnMoreUrl = helpContent.edge.condition.hasCondition.learnMoreUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
helpContent,
|
||||||
|
getHelpContent,
|
||||||
|
getNodeHelp,
|
||||||
|
getEdgeHelp
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user