mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-04-25 11:18:06 +00:00
192 lines
5.0 KiB
Vue
Executable File
192 lines
5.0 KiB
Vue
Executable File
<script setup>
|
|
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: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
data: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
isActive: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
sprite: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
})
|
|
|
|
// Expose hover events so parent can highlight edges
|
|
defineEmits(['hover', 'leave'])
|
|
|
|
// Walking animation state
|
|
const walkingFrame = ref(2) // Start with frame 2
|
|
let walkingInterval = null
|
|
|
|
// Compute properties for reactivity
|
|
const nodeType = computed(() => props.data?.type || 'unknown')
|
|
const nodeId = computed(() => props.data?.id || props.id)
|
|
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 ''
|
|
|
|
if (isActive.value) {
|
|
// When active, use walking frames (2 and 3)
|
|
return spriteFetcher.fetchSprite(nodeId.value, 'D', walkingFrame.value)
|
|
} else {
|
|
// When not active, use the original frame (1)
|
|
return props.sprite
|
|
}
|
|
})
|
|
|
|
// Start/stop walking animation based on active state
|
|
const startWalking = () => {
|
|
if (walkingInterval) return
|
|
|
|
walkingInterval = setInterval(() => {
|
|
walkingFrame.value = walkingFrame.value === 2 ? 3 : 2
|
|
}, 500) // Alternate every 500ms
|
|
}
|
|
|
|
const stopWalking = () => {
|
|
if (walkingInterval) {
|
|
clearInterval(walkingInterval)
|
|
walkingInterval = null
|
|
}
|
|
walkingFrame.value = 2 // Reset to frame 2
|
|
}
|
|
|
|
// Watch for active state changes
|
|
watch(isActive, (newActive) => {
|
|
if (newActive) {
|
|
startWalking()
|
|
} else {
|
|
stopWalking()
|
|
}
|
|
})
|
|
|
|
// Cleanup on unmount
|
|
onUnmounted(() => {
|
|
stopWalking()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<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>
|
|
<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>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.workflow-node-container {
|
|
position: relative;
|
|
}
|
|
|
|
.workflow-node-description {
|
|
/* Ensure long descriptions wrap instead of overflowing */
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
overflow-wrap: anywhere;
|
|
max-width: 200px;
|
|
display: block;
|
|
}
|
|
|
|
.workflow-node-sprite {
|
|
position: absolute;
|
|
top: -25px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 10;
|
|
}
|
|
|
|
.node-sprite-image {
|
|
width: 32px;
|
|
height: 40px;
|
|
object-fit: contain;
|
|
}
|
|
</style> |