Merge pull request #532 from LaansDole/feat/tooltip-enhancement

Feat: tooltip enhancement
This commit is contained in:
Shu Yao 2026-02-11 11:38:19 +08:00 committed by GitHub
commit 4a6ee25d79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 857 additions and 14 deletions

View 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>

View File

@ -21,6 +21,13 @@
</label>
<p class="setting-desc">Automatically expand message content in the chat view.</p>
</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 class="modal-footer">
<button class="cancel-button" @click="close">Cancel</button>
@ -44,7 +51,8 @@ const props = defineProps({
const localConfig = reactive({
AUTO_SHOW_ADVANCED: false,
AUTO_EXPAND_MESSAGES: false
AUTO_EXPAND_MESSAGES: false,
ENABLE_HELP_TOOLTIPS: true
})
watch(() => props.isVisible, (newVal) => {

View File

@ -1,6 +1,9 @@
<script setup>
import { computed } from 'vue'
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({
id: {
@ -12,10 +15,19 @@ const props = defineProps({
default: () => ({})
}
})
const shouldShowTooltip = computed(() => configStore.ENABLE_HELP_TOOLTIPS)
</script>
<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>
<!-- Provide source handle at right -->
<Handle id="source" type="source" :position="Position.Right" class="start-node-handle" />

View File

@ -2,6 +2,9 @@
import { computed, ref, nextTick, watch } from 'vue'
import { BaseEdge, EdgeLabelRenderer, getBezierPath, getSmoothStepPath, MarkerType } 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()
@ -562,6 +565,44 @@ const labelStyle = computed(() => {
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>
<template>
@ -614,6 +655,23 @@ const labelStyle = computed(() => {
:animated="false"
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">
<div
:key="edgeLabelKey"
@ -649,4 +707,13 @@ const labelStyle = computed(() => {
.animated-label {
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>

View File

@ -3,6 +3,9 @@ import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { Handle, Position } from '@vue-flow/core'
import { getNodeStyles } from '../utils/colorUtils.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({
id: {
@ -37,6 +40,10 @@ const nodeDescription = computed(() => props.data?.description || '')
const isActive = computed(() => props.isActive)
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
const currentSprite = computed(() => {
if (!props.sprite) return ''
@ -83,7 +90,43 @@ onUnmounted(() => {
</script>
<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">
<img :src="currentSprite" :alt="`${nodeId} sprite`" class="node-sprite-image" />
</div>

View File

@ -1,8 +1,11 @@
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import MarkdownIt from 'markdown-it'
import markdownItAnchor from 'markdown-it-anchor'
const route = useRoute()
const renderedContent = ref('')
const currentLang = ref('en') // 'zh' for Chinese, 'en' for English
const markdownBody = ref(null)
@ -28,6 +31,23 @@ md.use(markdownItAnchor, {
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 = () => {
nextTick(() => {
const container = markdownBody.value
@ -71,6 +91,7 @@ const loadTutorial = async () => {
renderedContent.value = md.render(text)
addCopyButtons()
scrollToHash()
} else {
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;
border: 1.5px solid #00eaff33;
transition: box-shadow 0.3s;
scroll-behavior: smooth;
}
.lang-switch {
@ -175,6 +197,23 @@ onMounted(() => {
color: #00eaff;
text-shadow: 0 0 8px #00eaff44;
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; }

View File

@ -87,7 +87,16 @@
>
<!-- Pane context menu -->
<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
v-else
class="context-menu-item"
@click.stop="() => { hideContextMenu(); openCreateNodeModal(); }"
>
@ -97,13 +106,31 @@
<!-- Node context menu -->
<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
v-else
class="context-menu-item"
@click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }"
>
Copy Node
</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
v-else
class="context-menu-item"
@click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }"
>
@ -113,7 +140,16 @@
<!-- Edge context menu -->
<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
v-else
class="context-menu-item"
@click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }"
>
@ -141,13 +177,28 @@
</button>
</div>
<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>
</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>
</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>
</button>
@ -166,11 +217,26 @@
</div>
<transition name="fade">
<div v-if="showMenu" class="menu-dropdown">
<div @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div>
<div @click="openCopyWorkflowModal" class="menu-item">Copy Workflow</div>
<div @click="openManageVarsModal" class="menu-item">Manage Variables</div>
<div @click="openManageMemoriesModal" class="menu-item">Manage Memories</div>
<div @click="openCreateEdgeModal" class="menu-item">Create Edge</div>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.renameWorkflow" placement="left">
<div @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div>
</RichTooltip>
<div v-else @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</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>
</transition>
</div>
@ -258,7 +324,7 @@
</template>
<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 { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
@ -270,8 +336,13 @@ import WorkflowNode from '../components/WorkflowNode.vue'
import WorkflowEdge from '../components/WorkflowEdge.vue'
import StartNode from '../components/StartNode.vue'
import FormGenerator from '../components/FormGenerator.vue'
import RichTooltip from '../components/RichTooltip.vue'
import yaml from 'js-yaml'
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({
workflowName: {

View File

@ -25,7 +25,24 @@ const routes = [
const router = createRouter({
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

View File

@ -4,7 +4,8 @@ const CONFIG_KEY = 'agent_config_settings'
const defaultSettings = {
AUTO_SHOW_ADVANCED: false,
AUTO_EXPAND_MESSAGES: false
AUTO_EXPAND_MESSAGES: false,
ENABLE_HELP_TOOLTIPS: true
}
// Initialize state from localStorage

View 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
}