Merge pull request #596 from nregret/feature/i18n-zh-support

feat: Add comprehensive i18n support 前端中文i18n
This commit is contained in:
Yufan Dang 2026-04-04 13:28:53 +08:00 committed by GitHub
commit 67172ac658
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1830 additions and 371 deletions

View File

@ -17,6 +17,7 @@
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0", "markdown-it-anchor": "^9.2.0",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-i18n": "^11.3.0",
"vue-router": "^4.6.0" "vue-router": "^4.6.0"
}, },
"devDependencies": { "devDependencies": {
@ -733,6 +734,67 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@intlify/core-base": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
"integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
"license": "MIT",
"dependencies": {
"@intlify/devtools-types": "11.3.0",
"@intlify/message-compiler": "11.3.0",
"@intlify/shared": "11.3.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/devtools-types": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
"integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.3.0",
"@intlify/shared": "11.3.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
"integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.3.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
"integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -2866,6 +2928,27 @@
"eslint": "^8.57.0 || ^9.0.0" "eslint": "^8.57.0 || ^9.0.0"
} }
}, },
"node_modules/vue-i18n": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.3.0",
"@intlify/devtools-types": "11.3.0",
"@intlify/shared": "11.3.0",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.6.4", "version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",

View File

@ -18,6 +18,7 @@
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0", "markdown-it-anchor": "^9.2.0",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-i18n": "^11.3.0",
"vue-router": "^4.6.0" "vue-router": "^4.6.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -4,7 +4,7 @@
<button <button
class="copy-btn" class="copy-btn"
@click="copyToClipboard" @click="copyToClipboard"
:title="copyStatus === 'copied' ? 'Copied!' : 'Copy original content'" :title="copyStatus === 'copied' ? $t('components.collapsible_message.copied') : $t('components.collapsible_message.copy_original')"
> >
<svg v-if="copyStatus === 'idle'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg v-if="copyStatus === 'idle'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
@ -280,3 +280,4 @@ watch(() => props.htmlContent, () => {
background-color: transparent; background-color: transparent;
} }
</style> </style>
/style>

View File

@ -3,11 +3,11 @@
<template v-if="field.childNode"> <template v-if="field.childNode">
<div v-if="recursive" class="form-group form-group-inline child-node-group"> <div v-if="recursive" class="form-group form-group-inline child-node-group">
<label> <label>
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -62,7 +62,7 @@
v-if="hasChildValue" v-if="hasChildValue"
class="clear-child-button" class="clear-child-button"
@click.stop="$emit('clear-child-entry', field.name)" @click.stop="$emit('clear-child-entry', field.name)"
title="Clear configuration" :title="$t('dynamic_form_field.clear_configuration')"
> >
× ×
</button> </button>
@ -87,7 +87,7 @@
<button <button
class="delete-child-button" class="delete-child-button"
@click.stop="$emit('delete-child-entry', field.name, childIndex)" @click.stop="$emit('delete-child-entry', field.name, childIndex)"
title="Delete child" :title="$t('dynamic_form_field.delete_child')"
> >
× ×
</button> </button>
@ -118,11 +118,11 @@
<!-- Standard Popup Style (if not expanded) --> <!-- Standard Popup Style (if not expanded) -->
<div v-if="!expandInline" class="form-group form-group-inline child-node-group"> <div v-if="!expandInline" class="form-group form-group-inline child-node-group">
<label> <label>
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -189,7 +189,7 @@
v-if="!canOpenConditionalChildModal" v-if="!canOpenConditionalChildModal"
class="child-node-hint" class="child-node-hint"
> >
Please select a type and configure {{ $t('dynamic_form_field.select_type_and_configure') }}
</div> </div>
<div <div
v-if="hasChildValue && childSummaries[field.name]" v-if="hasChildValue && childSummaries[field.name]"
@ -230,56 +230,55 @@
<!-- Choice dropdown fields--> <!-- Choice dropdown fields-->
<div v-if="field.enum && !isList" class="form-group"> <div v-if="field.enum && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`"> <label :for="`${modalId}-${field.name}`">
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
</RichTooltip> </RichTooltip>
</label> </label>
<div class="custom-select-wrapper" :class="{ 'select-disabled': isReadOnly }"> <div ref="selectWrapper" class="custom-select-wrapper" :class="{ 'select-disabled': isReadOnly }" @click="toggleDropdown">
<input <input
:id="`${modalId}-${field.name}`" :id="`${modalId}-${field.name}`"
:value="inputValue" :value="inputValue"
@input="onFilterInput"
@focus="showDropdown = true"
@blur="handleInputBlur"
class="form-input custom-select-input" class="form-input custom-select-input"
:readonly="isReadOnly" readonly
:class="{'input-readonly': isReadOnly}" :class="{'input-readonly': isReadOnly}"
placeholder="Type to filter options..." :placeholder="$t('dynamic_form_field.type_to_filter')"
autocomplete="off" autocomplete="off"
/> />
<div v-if="showDropdown && !isReadOnly" class="custom-select-dropdown"> <Teleport to="body">
<div <div v-if="showDropdown && !isReadOnly" class="custom-select-dropdown" :style="dropdownStyle">
v-if="!field.required" <div
class="custom-select-option" v-if="!field.required"
:class="{ 'option-selected': formData[field.name] === null }" class="custom-select-option"
@mousedown="selectOption(null)" :class="{ 'option-selected': formData[field.name] === null }"
> @mousedown.stop="selectOption(null)"
None >
None
</div>
<div
v-for="option in filteredOptions"
:key="typeof option === 'string' ? option : option.value"
class="custom-select-option"
:class="{ 'option-selected': formData[field.name] === (typeof option === 'string' ? option : option.value) }"
:title="typeof option === 'string' ? '' : (option.description || '')"
@mousedown.stop="selectOption(typeof option === 'string' ? option : option.value)"
>
{{
typeof option === 'string'
? option
: (option.label || option.value)
}}
</div>
<div v-if="filteredOptions.length === 0" class="custom-select-no-results">
{{ $t('dynamic_form_field.no_options_found') }}
</div>
</div> </div>
<div </Teleport>
v-for="option in filteredOptions"
:key="typeof option === 'string' ? option : option.value"
class="custom-select-option"
:class="{ 'option-selected': formData[field.name] === (typeof option === 'string' ? option : option.value) }"
:title="typeof option === 'string' ? '' : (option.description || '')"
@mousedown="selectOption(typeof option === 'string' ? option : option.value)"
>
{{
typeof option === 'string'
? option
: (option.label || option.value)
}}
</div>
<div v-if="filteredOptions.length === 0" class="custom-select-no-results">
No options found
</div>
</div>
<div class="custom-select-arrow" :class="{ 'arrow-open': showDropdown }"> <div class="custom-select-arrow" :class="{ 'arrow-open': showDropdown }">
</div> </div>
@ -289,11 +288,11 @@
<!-- Multi-choice fields --> <!-- Multi-choice fields -->
<div v-else-if="field.enum && isList" class="form-group"> <div v-else-if="field.enum && isList" class="form-group">
<label> <label>
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -326,11 +325,11 @@
<div v-else-if="field.type === 'bool' && !isList" class="form-group"> <div v-else-if="field.type === 'bool' && !isList" class="form-group">
<div class="switch-wrapper"> <div class="switch-wrapper">
<label :for="`${modalId}-${field.name}`" class="switch-label-text"> <label :for="`${modalId}-${field.name}`" class="switch-label-text">
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -352,11 +351,11 @@
<!-- String input fields --> <!-- String input fields -->
<div v-else-if="field.type === 'str' && !isList" class="form-group"> <div v-else-if="field.type === 'str' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`"> <label :for="`${modalId}-${field.name}`">
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -376,11 +375,11 @@
<!-- Multiline text fields --> <!-- Multiline text fields -->
<div v-else-if="field.type === 'text' && !isList" class="form-group"> <div v-else-if="field.type === 'text' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`"> <label :for="`${modalId}-${field.name}`">
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -400,11 +399,11 @@
<!-- Integer input fields --> <!-- Integer input fields -->
<div v-else-if="field.type === 'int' && !isList" class="form-group"> <div v-else-if="field.type === 'int' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`"> <label :for="`${modalId}-${field.name}`">
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -425,11 +424,11 @@
<!-- Float input fields --> <!-- Float input fields -->
<div v-else-if="field.type === 'float' && !isList" class="form-group"> <div v-else-if="field.type === 'float' && !isList" class="form-group">
<label :for="`${modalId}-${field.name}`"> <label :for="`${modalId}-${field.name}`">
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -450,11 +449,11 @@
<!-- Key-value fields --> <!-- Key-value fields -->
<div v-else-if="field.type === 'dict[str, Any]' || field.type === 'dict[str, str]'" class="form-group form-group-inline"> <div v-else-if="field.type === 'dict[str, Any]' || field.type === 'dict[str, str]'" class="form-group form-group-inline">
<label> <label>
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -475,7 +474,7 @@
/> />
</svg> </svg>
</span> </span>
Add key-value {{ $t('dynamic_form_field.add_key_value') }}
</button> </button>
<div <div
v-if="formData[field.name] && hasDictEntries(formData[field.name])" v-if="formData[field.name] && hasDictEntries(formData[field.name])"
@ -493,7 +492,7 @@
<button <button
class="delete-var-button" class="delete-var-button"
@click.stop="$emit('delete-var', field.name, varKey)" @click.stop="$emit('delete-var', field.name, varKey)"
title="Delete variable" :title="$t('dynamic_form_field.delete_variable')"
> >
× ×
</button> </button>
@ -505,11 +504,11 @@
<!-- List of strings field --> <!-- List of strings field -->
<div v-else-if="isList && field.type.includes('str')" class="form-group form-group-inline"> <div v-else-if="isList && field.type.includes('str')" class="form-group form-group-inline">
<label> <label>
{{ field.displayName || field.name }} {{ $te('schema.' + field.name) ? $t('schema.' + field.name) : (field.displayName || field.name) }}
<span v-if="field.required" class="required-asterisk">*</span> <span v-if="field.required" class="required-asterisk">*</span>
<RichTooltip <RichTooltip
v-if="field.description" v-if="field.description"
:content="{ description: field.description }" :content="{ description: $te('schema_desc.' + field.name) ? $t('schema_desc.' + field.name) : field.description }"
placement="top" placement="top"
> >
<span class="help-icon" tabindex="0">?</span> <span class="help-icon" tabindex="0">?</span>
@ -542,7 +541,7 @@
<button <button
class="delete-item-button" class="delete-item-button"
@click.stop="$emit('delete-list-item', field.name, index)" @click.stop="$emit('delete-list-item', field.name, index)"
title="Delete item" :title="$t('dynamic_form_field.delete_item')"
> >
× ×
</button> </button>
@ -555,9 +554,12 @@
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref, reactive, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import RichTooltip from './RichTooltip.vue' import RichTooltip from './RichTooltip.vue'
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
field: { field: {
type: Object, type: Object,
@ -607,6 +609,8 @@ const props = defineProps({
} }
}) })
const isReadOnly = computed(() => props.isReadOnly)
const emit = defineEmits([ const emit = defineEmits([
'update:form-data', 'update:form-data',
'open-child-modal', 'open-child-modal',
@ -629,6 +633,58 @@ const internalFormData = computed(() => props.formData)
// Custom select filtering state // Custom select filtering state
const showDropdown = ref(false) const showDropdown = ref(false)
const filterText = ref('') const filterText = ref('')
const selectWrapper = ref(null)
const dropdownRect = reactive({
top: 0,
left: 0,
width: 0
})
const updateDropdownPosition = () => {
if (selectWrapper.value) {
const rect = selectWrapper.value.getBoundingClientRect()
dropdownRect.top = rect.bottom
dropdownRect.left = rect.left
dropdownRect.width = rect.width
}
}
const dropdownStyle = computed(() => ({
position: 'fixed',
top: `${dropdownRect.top + 4}px`,
left: `${dropdownRect.left}px`,
width: `${dropdownRect.width}px`,
zIndex: 10000
}))
const toggleDropdown = () => {
if (props.isReadOnly) return
if (!showDropdown.value) {
updateDropdownPosition()
showDropdown.value = true
// Add escape listener
window.addEventListener('scroll', closeOnScroll, true)
window.addEventListener('mousedown', handleOutsideClick)
} else {
closeDropdown()
}
}
const closeDropdown = () => {
showDropdown.value = false
window.removeEventListener('scroll', closeOnScroll, true)
window.removeEventListener('mousedown', handleOutsideClick)
}
const closeOnScroll = () => {
if (showDropdown.value) closeDropdown()
}
const handleOutsideClick = (e) => {
if (selectWrapper.value && !selectWrapper.value.contains(e.target)) {
closeDropdown()
}
}
const isList = computed(() => { const isList = computed(() => {
return props.field.type && props.field.type.includes('list[') return props.field.type && props.field.type.includes('list[')
@ -648,9 +704,9 @@ const hasChildValue = computed(() => {
const childNodeButtonLabel = computed(() => { const childNodeButtonLabel = computed(() => {
if (isList.value) { if (isList.value) {
return `Add Entry` return t('dynamic_form_field.add_entry')
} }
return props.formData[props.field.name] ? `Edit ${props.field.childNode}` : `Configure ${props.field.childNode}` return props.formData[props.field.name] ? t('dynamic_form_field.edit_child', { child: props.field.childNode }) : t('dynamic_form_field.configure_child', { child: props.field.childNode })
}) })
// Filtered options for custom select // Filtered options for custom select
@ -725,8 +781,7 @@ const handleInputBlur = () => {
const selectOption = (value) => { const selectOption = (value) => {
props.formData[props.field.name] = value props.formData[props.field.name] = value
showDropdown.value = false closeDropdown()
filterText.value = ''
emit('handle-enum-change', props.field) emit('handle-enum-change', props.field)
} }
@ -1283,10 +1338,12 @@ input:checked + .switch-slider:before {
.custom-select-wrapper { .custom-select-wrapper {
position: relative; position: relative;
height: 38px; /* Fixed height to prevent absolute child from stretching parent */
} }
.custom-select-input { .custom-select-input {
cursor: pointer; cursor: pointer;
height: 100%;
} }
.custom-select-input:focus { .custom-select-input:focus {
@ -1296,7 +1353,202 @@ input:checked + .switch-slider:before {
.custom-select-arrow { .custom-select-arrow {
position: absolute; position: absolute;
right: 12px; right: 12px;
top: 18px; margin-left: 8px;
}
.delete-item-button:hover {
color: #ff6b6b;
}
.child-node-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.child-node-controls {
display: flex;
align-items: center;
gap: 5px;
}
.clear-child-button {
background: transparent;
border: none;
color: #999;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
line-height: 1;
}
.clear-child-button:hover {
color: #ff6b6b;
}
.add-child-button,
.add-list-button,
.add-var-button {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background-color: rgba(255, 255, 255, 0.14);
color: rgba(242, 242, 242, 0.9);
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
}
.add-child-button:hover,
.add-list-button:hover,
.add-var-button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.add-child-button .plus-icon,
.add-list-button .plus-icon,
.add-var-button .plus-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}
.add-child-button .plus-icon svg,
.add-list-button .plus-icon svg,
.add-var-button .plus-icon svg {
display: block;
}
.add-child-button:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: rgba(255, 255, 255, 0.1);
background-color: rgba(74, 74, 74, 0.8);
color: rgba(200, 200, 200, 0.7);
}
.child-node-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.child-node-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 14px;
background-color: rgba(90, 90, 90, 0.9);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
color: #f2f2f2;
font-size: 13px;
gap: 12px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.child-node-item:hover {
background-color: rgba(110, 110, 110, 0.95);
border-color: rgba(160, 196, 255, 0.7);
}
.child-node-name {
flex: 1;
}
.child-node-actions {
display: flex;
gap: 8px;
}
.child-node-item-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
}
.child-node-summary {
padding: 6px 12px;
margin-left: 0;
background-color: rgba(60, 60, 60, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: rgba(200, 200, 200, 0.85);
font-size: 12px;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-line;
}
.edit-child-button,
.delete-child-button {
padding: 6px 10px;
border-radius: 999px;
border: none;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background-color 0.2s ease, color 0.2s ease;
}
.edit-child-button {
background-color: rgba(117, 117, 117, 0.95);
color: #f2f2f2;
}
.edit-child-button:hover {
background-color: rgba(136, 136, 136, 0.98);
}
.delete-child-button {
background-color: rgba(90, 90, 90, 0.9);
color: #f2f2f2;
}
.delete-child-button:hover {
background-color: rgba(110, 110, 110, 0.95);
}
.child-node-hint {
padding: 6px 8px;
color: rgba(189, 189, 189, 0.9);
font-size: 12px;
}
/* Custom select styles */
.custom-select-wrapper {
position: relative;
height: 38px; /* Fixed height to prevent absolute child from stretching parent */
}
.custom-select-input {
cursor: pointer;
height: 100%;
}
.custom-select-input:focus {
cursor: text;
}
.custom-select-arrow {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: rgba(242, 242, 242, 0.7); color: rgba(242, 242, 242, 0.7);
font-size: 12px; font-size: 12px;
@ -1308,37 +1560,37 @@ input:checked + .switch-slider:before {
transform: translateY(-50%) rotate(180deg); transform: translateY(-50%) rotate(180deg);
} }
.custom-select-dropdown { :global(.custom-select-dropdown) {
position: static; position: fixed;
z-index: 10000;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
background-color: rgba(25, 25, 25, 0.95); background-color: #1e1e1e;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
margin-top: 2px; box-sizing: border-box;
width: 100%;
} }
.custom-select-option { :global(.custom-select-option) {
padding: 6px 6px; padding: 10px 14px;
cursor: pointer; cursor: pointer;
color: #f2f2f2; color: #f2f2f2;
font-size: 13px; font-size: 13px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
.custom-select-option:hover { :global(.custom-select-option:hover) {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.custom-select-option.option-selected { :global(.custom-select-option.option-selected) {
background-color: rgba(160, 196, 255, 0.2); background-color: rgba(160, 196, 255, 0.2);
color: #a0c4ff; color: #a0c4ff;
font-weight: 500; font-weight: 500;
} }
.custom-select-no-results { :global(.custom-select-no-results) {
padding: 10px 12px; padding: 10px 12px;
color: rgba(189, 189, 189, 0.7); color: rgba(189, 189, 189, 0.7);
font-size: 12px; font-size: 12px;

View File

@ -44,7 +44,7 @@
/> />
</div> </div>
<!-- Advanced Settings Toggle (Between Normal Fields and Inline Config) --> <!-- {{ $t('form_generator.advanced_settings') }} Toggle (Between Normal Fields and Inline Config) -->
<div <div
v-if="modal.hasAdvancedFields" v-if="modal.hasAdvancedFields"
class="advanced-toggle" class="advanced-toggle"
@ -54,7 +54,7 @@
class="advanced-toggle-button" class="advanced-toggle-button"
@click="toggleAdvancedFields(modal.id)" @click="toggleAdvancedFields(modal.id)"
> >
Advanced Settings {{ $t('form_generator.advanced_settings') }}
<span class="advanced-toggle-arrow"> <span class="advanced-toggle-arrow">
{{ modal.showAdvanced ? '▲' : '▼' }} {{ modal.showAdvanced ? '▲' : '▼' }}
</span> </span>
@ -100,7 +100,7 @@
class="add-child-button" class="add-child-button"
:disabled="modal.submitting" :disabled="modal.submitting"
> >
Copy Node {{ $t('form_generator.copy_node') }}
</button> </button>
<button <button
v-if="showDeleteButton(modal)" v-if="showDeleteButton(modal)"
@ -115,7 +115,7 @@
class="submit-button" class="submit-button"
:disabled="modal.submitting" :disabled="modal.submitting"
> >
Submit {{ $t('form_generator.submit') }}
</button> </button>
</div> </div>
</div> </div>
@ -133,7 +133,7 @@
{{ varFormError }} {{ varFormError }}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="var-key">Key <span class="required-asterisk">*</span></label> <label for="var-key">{{ $t('form_generator.key') }} <span class="required-asterisk">*</span></label>
<input <input
id="var-key" id="var-key"
v-model="varForm.key" v-model="varForm.key"
@ -142,7 +142,7 @@
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="var-value">Value <span class="required-asterisk">*</span></label> <label for="var-value">{{ $t('form_generator.value') }} <span class="required-asterisk">*</span></label>
<input <input
id="var-value" id="var-value"
v-model="varForm.value" v-model="varForm.value"
@ -151,8 +151,8 @@
/> />
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button @click="closeVarModal" class="cancel-button">Cancel</button> <button @click="closeVarModal" class="cancel-button">{{ $t('form_generator.cancel') }}</button>
<button @click="confirmVar" class="confirm-button">Confirm</button> <button @click="confirmVar" class="confirm-button">{{ $t('form_generator.confirm') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -166,7 +166,7 @@
> >
<div class="modal-content list-item-modal"> <div class="modal-content list-item-modal">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">{{ editingListItemIndex !== null ? 'Edit Entry' : 'Add Entry' }}</h3> <h3 class="modal-title">{{ editingListItemIndex !== null ? t('form_generator.edit_entry') : t('form_generator.add_entry') }}</h3>
<button class="close-button" @click="closeListItemModal">×</button> <button class="close-button" @click="closeListItemModal">×</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -174,7 +174,7 @@
{{ listItemFormError }} {{ listItemFormError }}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="list-item-value">Value <span class="required-asterisk">*</span></label> <label for="list-item-value">{{ $t('form_generator.value') }} <span class="required-asterisk">*</span></label>
<input <input
id="list-item-value" id="list-item-value"
v-model="listItemForm.value" v-model="listItemForm.value"
@ -183,8 +183,8 @@
/> />
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button @click="closeListItemModal" class="cancel-button">Cancel</button> <button @click="closeListItemModal" class="cancel-button">{{ $t('form_generator.cancel') }}</button>
<button @click="confirmListItem" class="confirm-button">Confirm</button> <button @click="confirmListItem" class="confirm-button">{{ $t('form_generator.confirm') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -193,6 +193,7 @@
<script setup> <script setup>
import { ref, reactive, watch, nextTick, computed, onMounted, onBeforeUnmount } from 'vue' import { ref, reactive, watch, nextTick, computed, onMounted, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import { fetchConfigSchema, postYaml, fetchWorkflowYAML, updateYaml } from '../utils/apiFunctions.js' import { fetchConfigSchema, postYaml, fetchWorkflowYAML, updateYaml } from '../utils/apiFunctions.js'
import { configStore } from '../utils/configStore.js' import { configStore } from '../utils/configStore.js'
@ -251,6 +252,8 @@ const props = defineProps({
const emit = defineEmits(['close', 'submit', 'copy']) const emit = defineEmits(['close', 'submit', 'copy'])
const { t } = useI18n()
// Stores all "layers" of forms // Stores all "layers" of forms
const modalStack = reactive([]) const modalStack = reactive([])
let modalIdCounter = 0 let modalIdCounter = 0
@ -608,7 +611,7 @@ const validateModalForm = (modal) => {
const value = modal.formData[field.name] const value = modal.formData[field.name]
if (isValueEmpty(value)) { if (isValueEmpty(value)) {
modal.formError = `"${field.displayName || field.name}" is required` modal.formError = t('form_generator.field_required', { field: field.displayName || field.name })
return false return false
} }
} }
@ -1155,15 +1158,15 @@ const generateChildSummary = (modal, formData) => {
// For list fields, show number of entries configured // For list fields, show number of entries configured
const entryCount = Array.isArray(value) ? value.length : 0 const entryCount = Array.isArray(value) ? value.length : 0
if (entryCount > 0 && entryCount===1) { if (entryCount > 0 && entryCount===1) {
summaryParts.push(`${label}: 1 entry configured`) summaryParts.push(t('form_generator.entry_configured_single', { label }))
} else if (entryCount > 0 && entryCount > 1) { } else if (entryCount > 0 && entryCount > 1) {
summaryParts.push(`${label}: ${entryCount} entries configured`) summaryParts.push(t('form_generator.entry_configured_multiple', { label, count: entryCount }))
} }
} else { } else {
// For non-list fields, show configuration status // For non-list fields, show configuration status
const isConfigured = value !== null && value !== undefined && value !== '' const isConfigured = value !== null && value !== undefined && value !== ''
if (isConfigured) { if (isConfigured) {
summaryParts.push(`${label}: Configured`) summaryParts.push(t('form_generator.configured', { label }))
} }
} }
continue continue
@ -1370,7 +1373,7 @@ const validateModalFormEnhanced = (modal) => {
// Inline modal has its own error display capabilities? // Inline modal has its own error display capabilities?
// Actually `DynamicFormField` doesn't render headers. // Actually `DynamicFormField` doesn't render headers.
// We might need to bubble the error to the parent. // We might need to bubble the error to the parent.
modal.formError = `Error in ${key}: ${childModal.formError || 'Invalid configuration'}` modal.formError = t('form_generator.error_in', { key, error: childModal.formError || t('form_generator.invalid_configuration') })
return false return false
} }
} }
@ -1405,12 +1408,12 @@ const showCopyButton = (modal) => {
const deleteButtonLabel = (modal) => { const deleteButtonLabel = (modal) => {
const field = getUpperBreadcrumbsField(modal) const field = getUpperBreadcrumbsField(modal)
if (field === 'nodes') { if (field === 'nodes') {
return 'Delete Node' return t('form_generator.delete_node')
} }
if (field === 'edges') { if (field === 'edges') {
return 'Delete Edge' return t('form_generator.delete_edge')
} }
return 'Delete' return t('form_generator.delete')
} }
const isNodeModal = (modal) => { const isNodeModal = (modal) => {
@ -1478,7 +1481,7 @@ const deleteEntry = async (modalId) => {
// Ask for user confirmation before deleting node/edge // Ask for user confirmation before deleting node/edge
const collectionField = getUpperBreadcrumbsField(modal) const collectionField = getUpperBreadcrumbsField(modal)
const entityLabel = collectionField === 'nodes' ? 'node' : collectionField === 'edges' ? 'edge' : 'entry' const entityLabel = collectionField === 'nodes' ? 'node' : collectionField === 'edges' ? 'edge' : 'entry'
const confirmed = window.confirm(`Are you sure you want to delete this ${entityLabel}?`) const confirmed = window.confirm(t('form_generator.confirm_delete', { entity: entityLabel }))
if (!confirmed) { if (!confirmed) {
return return
} }
@ -1490,7 +1493,7 @@ const deleteEntry = async (modalId) => {
if (props.workflowName) { if (props.workflowName) {
const ready = await ensureBaseYamlReady() const ready = await ensureBaseYamlReady()
if (!ready || !baseYamlObject.value) { if (!ready || !baseYamlObject.value) {
modal.formError = baseYamlError.value || 'Failed to load existing workflow YAML' modal.formError = baseYamlError.value || t('form_generator.failed_to_load_yaml')
return return
} }
} }
@ -1498,7 +1501,7 @@ const deleteEntry = async (modalId) => {
// filename without the .yaml suffix // filename without the .yaml suffix
const filename = resolveTargetFilename() const filename = resolveTargetFilename()
if (!filename) { if (!filename) {
modal.formError = 'Graph ID is required to save workflow' modal.formError = t('form_generator.graph_id_required')
return return
} }
@ -1507,14 +1510,14 @@ const deleteEntry = async (modalId) => {
: null : null
if (!yamlObjectToSave) { if (!yamlObjectToSave) {
modal.formError = 'No workflow context available for deletion' modal.formError = t('form_generator.no_workflow_context')
return return
} }
if (collectionField === 'nodes') { if (collectionField === 'nodes') {
const targetNodeId = resolveNodeIdForDeletion(modal) const targetNodeId = resolveNodeIdForDeletion(modal)
if (!targetNodeId) { if (!targetNodeId) {
modal.formError = 'Node ID is required for deletion' modal.formError = t('form_generator.node_id_required')
return return
} }
removeNodeRelatedYamlInfo(yamlObjectToSave, targetNodeId) removeNodeRelatedYamlInfo(yamlObjectToSave, targetNodeId)
@ -1526,7 +1529,7 @@ const deleteEntry = async (modalId) => {
const result = await saveWorkflowYaml(filename, yamlString) const result = await saveWorkflowYaml(filename, yamlString)
if (!result.success) { if (!result.success) {
modal.formError = result.detail || result.message || 'Failed to save workflow' modal.formError = result.detail || result.message || t('form_generator.failed_to_save')
return return
} }
@ -1544,7 +1547,7 @@ const deleteEntry = async (modalId) => {
closeModal(modalId) closeModal(modalId)
} catch (error) { } catch (error) {
console.error('Error deleting entry:', error) console.error('Error deleting entry:', error)
modal.formError = 'Failed to delete entry' modal.formError = t('form_generator.failed_to_delete')
} finally { } finally {
modal.submitting = false modal.submitting = false
} }
@ -1603,7 +1606,7 @@ const submitForm = async (modalId) => {
const { structuredData } = buildPartialYamlPayload(modal, payload) const { structuredData } = buildPartialYamlPayload(modal, payload)
if (!structuredData) { if (!structuredData) {
modal.formError = 'Failed to generate YAML output' modal.formError = t('form_generator.failed_to_generate_yaml')
return return
} }
@ -1662,7 +1665,7 @@ const submitForm = async (modalId) => {
closeModal(modalId) closeModal(modalId)
} catch (error) { } catch (error) {
console.error('Error submitting form:', error) console.error('Error submitting form:', error)
modal.formError = 'Failed to submit form, error:\n' + error modal.formError = t('form_generator.failed_to_submit') + error
} finally { } finally {
modal.submitting = false modal.submitting = false
} }
@ -1781,7 +1784,7 @@ const openChildModal = async (modalId, field, listIndex = null) => {
try { try {
await generateForm(nextBreadcrumbs, true, context, initialData) await generateForm(nextBreadcrumbs, true, context, initialData)
} catch (error) { } catch (error) {
parentModal.formError = 'Failed to load child schema, error:\n' + error parentModal.formError = t('form_generator.failed_to_load_child') + error
} }
} }
@ -1942,17 +1945,17 @@ const editVar = (modalId, fieldName, key) => {
const validateVarForm = (modal) => { const validateVarForm = (modal) => {
if (!varForm.key.trim()) { if (!varForm.key.trim()) {
varFormError.value = 'Key is required' varFormError.value = t('form_generator.key_required')
return false return false
} }
if (!varForm.value.trim()) { if (!varForm.value.trim()) {
varFormError.value = 'Value is required' varFormError.value = t('form_generator.value_required')
return false return false
} }
const fieldName = activeVarFieldName.value const fieldName = activeVarFieldName.value
if (!fieldName) { if (!fieldName) {
varFormError.value = 'Invalid field context' varFormError.value = t('form_generator.invalid_field_context')
return false return false
} }
@ -1966,7 +1969,7 @@ const validateVarForm = (modal) => {
Object.prototype.hasOwnProperty.call(modal.formData[fieldName], trimmedKey) && Object.prototype.hasOwnProperty.call(modal.formData[fieldName], trimmedKey) &&
editingVarIndex.value !== trimmedKey editingVarIndex.value !== trimmedKey
) { ) {
varFormError.value = 'Key already exists' varFormError.value = t('form_generator.key_exists')
return false return false
} }
@ -2047,7 +2050,7 @@ const resetListItemForm = () => {
const validateListItemForm = () => { const validateListItemForm = () => {
if (!listItemForm.value.trim()) { if (!listItemForm.value.trim()) {
listItemFormError.value = 'Value is required' listItemFormError.value = t('form_generator.value_required')
return false return false
} }
return true return true

View File

@ -39,7 +39,7 @@
class="advanced-toggle-button" class="advanced-toggle-button"
@click="$emit('toggle-advanced-fields', modal.id)" @click="$emit('toggle-advanced-fields', modal.id)"
> >
Advanced Settings {{ $t('components.inline_config.advanced_settings') }}
<span class="advanced-toggle-arrow"> <span class="advanced-toggle-arrow">
{{ modal.showAdvanced ? '▲' : '▼' }} {{ modal.showAdvanced ? '▲' : '▼' }}
</span> </span>

View File

@ -26,10 +26,13 @@
@mouseleave="handleTooltipMouseLeave" @mouseleave="handleTooltipMouseLeave"
> >
<div class="tooltip-content"> <div class="tooltip-content">
<h4 v-if="content.title" class="tooltip-title">{{ content.title }}</h4> <h4 v-if="content.title" class="tooltip-title">{{ $t(content.title) }}</h4>
<p class="tooltip-description">{{ content.description }}</p> <template v-if="content.descriptions && content.descriptions.length">
<p v-for="(desc, index) in content.descriptions" :key="'desc-'+index" class="tooltip-description">{{ $t(desc) }}</p>
</template>
<p v-else-if="content.description" class="tooltip-description">{{ $t(content.description) }}</p>
<ul v-if="content.examples && content.examples.length" class="tooltip-examples"> <ul v-if="content.examples && content.examples.length" class="tooltip-examples">
<li v-for="(example, index) in content.examples" :key="index">{{ example }}</li> <li v-for="(example, index) in content.examples" :key="index">{{ $t(example) }}</li>
</ul> </ul>
<a <a
v-if="content.learnMoreUrl" v-if="content.learnMoreUrl"
@ -39,7 +42,7 @@
class="tooltip-learn-more" class="tooltip-learn-more"
@click="handleLearnMore" @click="handleLearnMore"
> >
Learn More {{ $t('components.rich_tooltip.learn_more') }}
</a> </a>
</div> </div>
<div class="tooltip-arrow" :data-placement="placement"></div> <div class="tooltip-arrow" :data-placement="placement"></div>

View File

@ -3,35 +3,43 @@
<div v-if="isVisible" class="modal-overlay" @click.self="close"> <div v-if="isVisible" class="modal-overlay" @click.self="close">
<div class="modal-content settings-modal"> <div class="modal-content settings-modal">
<div class="modal-header"> <div class="modal-header">
<h3>Settings</h3> <h3>{{ $t('settings.title') }}</h3>
<button class="close-button" @click="close">×</button> <button class="close-button" @click="close">×</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="settings-item"> <div class="settings-item">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" v-model="localConfig.AUTO_SHOW_ADVANCED"> <input type="checkbox" v-model="localConfig.AUTO_SHOW_ADVANCED">
Auto show advanced setting {{ $t('settings.auto_show_advanced') }}
</label> </label>
<p class="setting-desc">Automatically expand "Advanced Settings" in configuration forms.</p> <p class="setting-desc">{{ $t('settings.auto_show_advanced_desc') }}</p>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" v-model="localConfig.AUTO_EXPAND_MESSAGES"> <input type="checkbox" v-model="localConfig.AUTO_EXPAND_MESSAGES">
Automatically expand messages {{ $t('settings.auto_expand_messages') }}
</label> </label>
<p class="setting-desc">Automatically expand message content in the chat view.</p> <p class="setting-desc">{{ $t('settings.auto_expand_messages_desc') }}</p>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" v-model="localConfig.ENABLE_HELP_TOOLTIPS"> <input type="checkbox" v-model="localConfig.ENABLE_HELP_TOOLTIPS">
Enable help tooltips {{ $t('settings.enable_help_tooltips') }}
</label> </label>
<p class="setting-desc">Show contextual help tooltips throughout the workflow interface.</p> <p class="setting-desc">{{ $t('settings.enable_help_tooltips_desc') }}</p>
</div>
<div class="settings-item">
<label class="setting-label">{{ $t('settings.language') }}</label>
<select v-model="localConfig.LANGUAGE" class="language-select">
<option value="en">English</option>
<option value="zh">简体中文</option>
</select>
<p class="setting-desc">{{ $t('settings.language_desc') }}</p>
</div> </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">{{ $t('common.cancel') }}</button>
<button class="confirm-button" @click="save">Save</button> <button class="confirm-button" @click="save">{{ $t('common.save') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -41,6 +49,9 @@
<script setup> <script setup>
import { reactive, watch } from 'vue' import { reactive, watch } from 'vue'
import { configStore } from '../utils/configStore.js' import { configStore } from '../utils/configStore.js'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const props = defineProps({ const props = defineProps({
isVisible: { isVisible: {
@ -52,7 +63,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 ENABLE_HELP_TOOLTIPS: true,
LANGUAGE: 'en'
}) })
watch(() => props.isVisible, (newVal) => { watch(() => props.isVisible, (newVal) => {
@ -72,6 +84,7 @@ const close = () => {
const save = () => { const save = () => {
// Commit local changes to global store // Commit local changes to global store
Object.assign(configStore, localConfig) Object.assign(configStore, localConfig)
locale.value = localConfig.LANGUAGE
close() close()
} }
</script> </script>
@ -146,6 +159,23 @@ const save = () => {
padding-bottom: 0; padding-bottom: 0;
} }
.setting-label {
display: block;
color: #e0e0e0;
font-size: 15px;
margin-bottom: 8px;
}
.language-select {
width: 100%;
padding: 8px;
background: #2a2a2a;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
margin-bottom: 6px;
}
.checkbox-label { .checkbox-label {
display: flex; display: flex;
align-items: center; align-items: center;
@ -220,4 +250,4 @@ const save = () => {
.modal-fade-leave-to { .modal-fade-leave-to {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@ -2,17 +2,17 @@
<div class="sidebar" :class="{ 'is-hidden': isHidden }" @mouseenter="isHidden = false"> <div class="sidebar" :class="{ 'is-hidden': isHidden }" @mouseenter="isHidden = false">
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<router-link to="/">Home</router-link> <router-link to="/">{{ $t('nav.home') }}</router-link>
<router-link to="/tutorial">Tutorial</router-link> <router-link to="/tutorial">{{ $t('nav.tutorial') }}</router-link>
<router-link <router-link
to="/workflows" to="/workflows"
:class="{ active: isWorkflowsActive }" :class="{ active: isWorkflowsActive }"
>Workflows</router-link> >{{ $t('nav.workflows') }}</router-link>
<router-link to="/launch" target="_blank" rel="noopener">Launch</router-link> <router-link to="/launch" target="_blank" rel="noopener">{{ $t('nav.launch') }}</router-link>
<router-link to="/batch-run" target="_blank" rel="noopener">Labaratory</router-link> <router-link to="/batch-run" target="_blank" rel="noopener">{{ $t('nav.laboratory') }}</router-link>
</nav> </nav>
<div class="sidebar-actions"> <div class="sidebar-actions">
<button class="settings-nav-btn" @click="showSettingsModal = true" title="Settings"> <button class="settings-nav-btn" @click="showSettingsModal = true" :title="$t('settings.title')">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle> <circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>

View File

@ -22,13 +22,13 @@ const shouldShowTooltip = computed(() => configStore.ENABLE_HELP_TOOLTIPS)
<template> <template>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.startNode" placement="right"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.startNode" placement="right">
<div class="start-node" :style="{ opacity: data.opacity ?? 1 }"> <div class="start-node" :style="{ opacity: data.opacity ?? 1 }">
<div class="start-node-bubble" title="Start Node"></div> <div class="start-node-bubble" :title="$t('components.start_node.title')"></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" />
</div> </div>
</RichTooltip> </RichTooltip>
<div v-else class="start-node" :style="{ opacity: data.opacity ?? 1 }"> <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="$t('components.start_node.title')"></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" />
</div> </div>

View File

@ -5,8 +5,10 @@ import { useVueFlow } from '@vue-flow/core'
import RichTooltip from './RichTooltip.vue' import RichTooltip from './RichTooltip.vue'
import { getEdgeHelp } from '../utils/helpContent.js' import { getEdgeHelp } from '../utils/helpContent.js'
import { configStore } from '../utils/configStore.js' import { configStore } from '../utils/configStore.js'
import { useI18n } from 'vue-i18n'
const { findNode } = useVueFlow() const { findNode } = useVueFlow()
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
id: { id: {
@ -512,13 +514,13 @@ const edgeLabel = computed(() => {
const config = condition.config || {} const config = condition.config || {}
if (config.any && Array.isArray(config.any) && config.any.length > 0) { if (config.any && Array.isArray(config.any) && config.any.length > 0) {
parts.push(`Includes: ${config.any.join(', ')}`) parts.push(t('components.workflow_edge.includes', { values: config.any.join(', ') }))
} }
if (config.none && Array.isArray(config.none) && config.none.length > 0) { if (config.none && Array.isArray(config.none) && config.none.length > 0) {
parts.push(`Excludes: ${config.none.join(', ')}`) parts.push(t('components.workflow_edge.excludes', { values: config.none.join(', ') }))
} }
if (config.regex && Array.isArray(config.regex) && config.regex.length > 0) { if (config.regex && Array.isArray(config.regex) && config.regex.length > 0) {
parts.push(`Regex: ${config.regex.join(', ')}`) parts.push(t('components.workflow_edge.regex', { values: config.regex.join(', ') }))
} }
return parts.join('\n') return parts.join('\n')

25
frontend/src/i18n.js Normal file
View File

@ -0,0 +1,25 @@
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
import zh from './locales/zh.json'
const CONFIG_KEY = 'agent_config_settings'
const stored = localStorage.getItem(CONFIG_KEY)
let savedLanguage = 'en'
if (stored) {
try {
savedLanguage = JSON.parse(stored).LANGUAGE || 'en'
} catch (e) {}
}
const i18n = createI18n({
locale: savedLanguage,
fallbackLocale: 'en',
messages: {
en,
zh
},
legacy: false,
globalInjection: true
})
export default i18n

View File

@ -0,0 +1,507 @@
{
"components": {
"workflow_edge": {
"includes": "Includes: {values}",
"excludes": "Excludes: {values}",
"regex": "Regex: {values}"
},
"start_node": {
"title": "Start Node"
},
"rich_tooltip": {
"learn_more": "Learn More →"
},
"inline_config": {
"advanced_settings": "Advanced Settings"
},
"collapsible_message": {
"copied": "Copied!",
"copy_original": "Copy original content",
"show_more": "Show More",
"show_less": "Show Less"
}
},
"settings": {
"title": "Settings",
"auto_show_advanced": "Auto show advanced setting",
"auto_show_advanced_desc": "Automatically expand \"Advanced Settings\" in configuration forms.",
"auto_expand_messages": "Automatically expand messages",
"auto_expand_messages_desc": "Automatically expand message content in the chat view.",
"enable_help_tooltips": "Enable help tooltips",
"enable_help_tooltips_desc": "Show contextual help tooltips throughout the workflow interface.",
"language": "Language / 语言",
"language_desc": "Choose your preferred language."
},
"common": {
"cancel": "Cancel",
"save": "Save",
"submit": "Submit",
"download": "Download",
"retry": "Retry",
"search": "Search"
},
"nav": {
"home": "Home",
"tutorial": "Tutorial",
"workflows": "Workflows",
"launch": "Launch",
"laboratory": "Labaratory"
},
"tutorial": {
"copy": "Copy",
"copied": "Copied",
"failed_to_copy": "Failed to copy code: ",
"failed_to_fetch": "Failed to fetch tutorial markdown",
"failed_to_load": "Failed to load tutorial:",
"zh": "中文",
"en": "English"
},
"home": {
"title": "ChatDev 2.0",
"highlight": "DevAll",
"introduction": "ChatDev 2.0 - DevAll is a zero-code multi-agent platform for developing everything, with a workspace built for designing, visualizing, and running agent workflows.",
"get_started": "Get Started →"
},
"workbench": {
"select_workflow": "Select a workflow",
"choose_workflow": "Choose a workflow from the list to view or edit."
},
"workflow_list": {
"create": "Create Workflow",
"loading": "Loading workflows...",
"no_files": "No workflow files found"
},
"workflow_view": {
"yaml_parse_error": "YAML Parse Error: ",
"loading_yaml": "Loading YAML content...",
"create_node": "Create Node",
"copy_node": "Copy Node",
"delete_node": "Delete Node",
"delete_edge": "Delete Edge",
"workflow_graph": "Workflow Graph",
"yaml_configuration": "YAML Configuration",
"configure_graph": "Configure Graph",
"launch": "Launch",
"rename_workflow": "Rename Workflow",
"copy_workflow": "Copy Workflow",
"manage_variables": "Manage Variables",
"manage_memories": "Manage Memories",
"create_edge": "Create Edge",
"enter_new_name": "Enter new workflow name",
"workflow_name": "Workflow Name"
},
"batch_run": {
"title": "Labaratory",
"batch_settings": "Batch Settings",
"logs_placeholder": "Batch processing logs will appear here...",
"rows_completed": "Rows Completed",
"total_time": "Total Time",
"success_rate": "Success Rate",
"current_status": "Current Status",
"overall_progress": "Overall Progress",
"workflow_selection": "Workflow Selection",
"loading": "Loading...",
"select_yaml": "Select YAML file...",
"no_results": "No results",
"input_file_selection": "Input File Selection",
"select_input_file": "Select input file...",
"input_file_format": "Input File Format",
"view": "View",
"dashboard": "Dashboard",
"terminal": "Terminal",
"max_parallel": "Max. Parallel Launches",
"max_parallel_desc": "Maximum number of parallel workflow launches",
"log_level": "Log Level",
"log_level_desc": "Logging verbosity level",
"download_logs": "Download Logs",
"status_completed": "Batch completed",
"status_cancelled": "Batch cancelled",
"status_in_progress": "In Progress",
"status_idle": "Idle",
"status_waiting_workflow": "Waiting for workflow selection...",
"status_waiting_file": "Waiting for input file selection...",
"manual_intro": "Input file should contain at least task and/or attachments columns",
"manual_id": "Must be unique, auto-generated if column not found",
"manual_task": "Holds user input",
"manual_vars": "JSON object containing key-value pairs of global variables",
"manual_attachments": "JSON array containing absolute file paths of attachments for workflow",
"launch": "Launch",
"relaunch": "Relaunch"
},
"launch": {
"title": "Launch",
"collapse_chat": "Collapse chat",
"expand_chat": "Expand chat",
"loading_image": "Loading image...",
"preparing": "Preparing...",
"upload_file": "Upload File",
"uploading": "Uploading...",
"no_files_uploaded": "No files uploaded",
"drop_files": "Drop files to upload",
"enter_prompt": "Please enter task prompt...",
"status": "Status",
"view": "View",
"chat": "Chat",
"graph": "Graph",
"send": "Send",
"relaunch": "Relaunch",
"download_logs": "Download Logs",
"settings": "Settings",
"download": "Download",
"workflow_selection": "Workflow Selection",
"loading": "Loading...",
"select_yaml": "Select YAML file...",
"no_results": "No results",
"status_waiting_workflow": "Waiting for workflow selection...",
"status_connecting": "Connecting...",
"status_waiting_launch": "Waiting for launch...",
"status_running": "Running...",
"status_completed": "Completed",
"status_cancelled": "Cancelled",
"status_failed": "Failed",
"status_error": "Error",
"status_disconnected": "Disconnected",
"status_waiting_input": "Waiting for input...",
"status_launching": "Launching...",
"status_connection_error": "Connection error",
"launch_button": "Launch",
"alert_session_not_ready": "Session is not ready yet. Please wait for connection.",
"alert_failed_upload": "Failed to upload file",
"alert_file_upload_failed": "File upload failed, please try again.",
"alert_recording_error": "Recording error occurred",
"alert_mic_access_failed": "Failed to access microphone. Please check permissions.",
"alert_recording_upload_failed": "Recording upload failed, please try again.",
"alert_choose_workflow": "Please choose a workflow file",
"alert_ws_error": "WebSocket connection error!",
"alert_missing_session": "Missing session information from server.",
"alert_enter_prompt": "Please enter task prompt or upload files.",
"alert_ws_not_ready": "WebSocket connection is not ready yet.",
"alert_failed_launch": "Failed to launch workflow",
"unknown_error": "Unknown error",
"alert_failed_execute": "Failed to call execute API",
"alert_download_failed": "Failed to download file, please try again.",
"alert_download_logs_failed": "Download failed, please try again later",
"no_initial_instructions": "No initial instructions provided",
"workflow_cancelled": "Workflow cancelled"
},
"form_generator": {
"advanced_settings": "Advanced Settings",
"copy_node": "Copy Node",
"submit": "Submit",
"key": "Key",
"value": "Value",
"cancel": "Cancel",
"confirm": "Confirm",
"edit_entry": "Edit Entry",
"add_entry": "Add Entry",
"delete_node": "Delete Node",
"delete_edge": "Delete Edge",
"delete": "Delete",
"field_required": "\"{field}\" is required",
"failed_to_load_yaml": "Failed to load existing workflow YAML",
"graph_id_required": "Graph ID is required to save workflow",
"no_workflow_context": "No workflow context available for deletion",
"node_id_required": "Node ID is required for deletion",
"failed_to_save": "Failed to save workflow",
"failed_to_delete": "Failed to delete entry",
"failed_to_generate_yaml": "Failed to generate YAML output",
"failed_to_submit": "Failed to submit form, error:\n",
"failed_to_load_child": "Failed to load child schema, error:\n",
"key_required": "Key is required",
"value_required": "Value is required",
"invalid_field_context": "Invalid field context",
"key_exists": "Key already exists",
"confirm_delete": "Are you sure you want to delete this {entity}?",
"entry_configured_single": "{label}: 1 entry configured",
"entry_configured_multiple": "{label}: {count} entries configured",
"configured": "{label}: Configured",
"invalid_configuration": "Invalid configuration",
"error_in": "Error in {key}: {error}"
},
"dynamic_form_field": {
"type_to_filter": "Type to filter options...",
"none": "None",
"no_options_found": "No options found",
"add_key_value": "Add key-value",
"add_entry": "Add Entry",
"select_type_and_configure": "Please select a type and configure",
"clear_configuration": "Clear configuration",
"delete_child": "Delete child",
"delete_variable": "Delete variable",
"delete_item": "Delete item",
"edit_child": "Edit {child}",
"configure_child": "Configure {child}",
"configure": "Configure"
},
"schema": {
"agent_role": "Agent Role",
"model": "Model",
"temperature": "Temperature",
"max_tokens": "Max Tokens",
"top_p": "Top P",
"presence_penalty": "Presence Penalty",
"frequency_penalty": "Frequency Penalty",
"prompt": "Prompt",
"description": "Description",
"graph": "Graph",
"nodes": "Nodes",
"edges": "Edges",
"start": "Start",
"end": "End",
"vars": "Variables",
"memory": "Memory",
"id": "ID",
"name": "Name",
"type": "Type",
"config": "Configuration",
"condition": "Condition",
"trigger": "Trigger",
"from": "From",
"to": "To",
"version": "Version",
"is_majority_voting": "Majority Voting",
"log_level": "Log Level",
"context_window": "Context Window",
"provider": "Provider",
"base_url": "Base URL",
"api_key": "API Key",
"params": "Parameters",
"tooling": "Tooling",
"tools": "Tools",
"auto_fill": "Auto Fill",
"timeout": "Timeout",
"server": "Server",
"headers": "Headers",
"command": "Command",
"args": "Arguments",
"cwd": "CWD",
"env": "Environment",
"inherit_env": "Inherit Env",
"startup_timeout": "Startup Timeout",
"wait_for_log": "Wait for Log",
"thinking": "Thinking",
"reflection_prompt": "Reflection Prompt",
"memories": "Memories",
"retrieve_stage": "Retrieve Stage",
"top_k": "Top K",
"similarity_threshold": "Similarity Threshold",
"read": "Read",
"write": "Write",
"retry": "Retry",
"enabled": "Enabled",
"max_attempts": "Max Attempts",
"min_wait_seconds": "Min Wait Seconds",
"max_wait_seconds": "Max Wait Seconds",
"retry_on_status_codes": "Retry Status Codes",
"retry_on_exception_types": "Retry Exception Types",
"non_retry_exception_types": "Non-Retry Exception Types",
"retry_on_error_substrings": "Retry Error Substrings",
"initial_instruction": "Initial Instruction",
"organization": "Organization"
},
"schema_desc": {
"agent_role": "Defines the system-level role, persona, or behavioral guidelines.",
"model": "Specifies the underlying LLM configuration to use.",
"temperature": "Controls randomness or creativity of the model.",
"max_tokens": "Maximum number of tokens to generate per response.",
"top_p": "Considers cumulative probability mass for token sampling.",
"presence_penalty": "Penalizes words based on their presence to encourage new topics.",
"frequency_penalty": "Penalizes words based on their frequency to reduce repetition.",
"prompt": "Enter the specific prompt or detailed instruction for this node's task.",
"description": "Human-readable narrative explaining this workflow or node's goal.",
"graph": "The underlying definition and configuration of the graph.",
"nodes": "The sequence of processing nodes contained in this workflow.",
"edges": "Directed data edges defining how nodes connect.",
"start": "Target start nodes for workflow entry.",
"end": "Target end nodes for final workflow data collection.",
"vars": "Global parameters that can be universally referenced.",
"memory": "Optional memory stores that can be continuously referenced.",
"id": "Identifier for referencing. No spaces allowed.",
"name": "Short, human-readable label.",
"type": "Defines the execution mechanism of the component.",
"config": "Detailed parameter configuration pool.",
"condition": "Data will only traverse if this computes to true.",
"trigger": "Specified if this edge triggers downstream execution.",
"from": "Data source startpoint of the edge.",
"to": "Data target endpoint of the edge.",
"version": "Configuration version number.",
"is_majority_voting": "Decides whether the workflow computes a majority vote logic.",
"log_level": "Runtime log verbosity.",
"context_window": "Maximum amount of context tokens the model can read at once.",
"provider": "LLM API provider (OpenAI, Anthropic, etc).",
"base_url": "Overrides default API base URL.",
"api_key": "API Key token for the LLM service.",
"params": "Additional custom parameters passed to the model.",
"tooling": "Enables callable external functions.",
"tools": "List of available callable tools.",
"auto_fill": "Auto completes mandatory missing data elements.",
"timeout": "Maximum wait time for a single calling session.",
"server": "Target server configs.",
"headers": "Custom HTTP headers for the network request.",
"command": "Underlying shell command to be executed.",
"args": "Optional array of shell command arguments.",
"cwd": "Current working directory setting for environments.",
"env": "System Env vars map.",
"inherit_env": "Specifies whether to inherit host env variables.",
"startup_timeout": "Wait timespan tolerance for initial startup.",
"wait_for_log": "Checks log line output to ensure readiness.",
"thinking": "Enables inner reasoning mode.",
"reflection_prompt": "Used to instruct self-correction mechanisms.",
"memories": "External, persistent retrieve storage points.",
"retrieve_stage": "Stage instruction for retrieval context engine.",
"top_k": "How many related chunk blocks should be returned max.",
"similarity_threshold": "Minimum allowed similarity scale threshold.",
"read": "Ability toggle to read externally.",
"write": "Ability toggle to write externally.",
"retry": "Configuration logic when handling temporary failures.",
"enabled": "Whether this piece of module is activated.",
"max_attempts": "Maximum amount of retries acceptable.",
"min_wait_seconds": "Initial shortest wait time.",
"max_wait_seconds": "The ceiling backoff wait time.",
"retry_on_status_codes": "Activate retry handling upon hitting specific error codes.",
"retry_on_exception_types": "Trigger retry mechanisms only if the exception matches.",
"non_retry_exception_types": "Exceptions mapped as permanently fatal that will never be retried.",
"retry_on_error_substrings": "Retry triggered by particular error text strings.",
"initial_instruction": "Global contextual starter instruction."
},
"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"
]
},
"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)"
]
},
"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"
]
},
"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"
]
},
"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"
]
},
"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"
]
},
"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"
]
},
"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"
]
},
"unknown": {
"title": "Workflow Node",
"description": "A node in your workflow. Click to view and edit its configuration."
}
},
"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"
]
},
"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."
}
}
},
"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."
}
}
}
}

View File

@ -0,0 +1,507 @@
{
"settings": {
"title": "设置",
"auto_show_advanced": "自动显示高级设置",
"auto_show_advanced_desc": "在配置表单中自动展开“高级设置”。",
"auto_expand_messages": "自动展开消息",
"auto_expand_messages_desc": "在聊天视图中自动展开消息内容。",
"enable_help_tooltips": "启用帮助提示",
"enable_help_tooltips_desc": "在整个工作流界面中显示上下文帮助提示。",
"language": "语言 / Language",
"language_desc": "选择您的首选语言。"
},
"common": {
"cancel": "取消",
"save": "保存",
"submit": "提交",
"download": "下载",
"retry": "重试",
"search": "搜索"
},
"nav": {
"home": "主页",
"tutorial": "教程",
"workflows": "工作流",
"launch": "启动",
"laboratory": "实验室"
},
"tutorial": {
"copy": "复制",
"copied": "已复制",
"failed_to_copy": "复制代码失败:",
"failed_to_fetch": "获取教程 markdown 失败",
"failed_to_load": "加载教程失败:",
"zh": "中文",
"en": "English"
},
"home": {
"title": "ChatDev 2.0",
"highlight": "DevAll",
"introduction": "ChatDev 2.0 - DevAll 是一个用于开发万物的零代码多智能体平台,提供用于设计、可视化和运行智能体工作流的工作区。",
"get_started": "开始使用 →"
},
"workbench": {
"select_workflow": "选择工作流",
"choose_workflow": "从列表中选择要查看或编辑的工作流。"
},
"workflow_list": {
"create": "创建工作流",
"loading": "加载工作流中...",
"no_files": "未找到工作流文件"
},
"workflow_view": {
"yaml_parse_error": "YAML 解析错误:",
"loading_yaml": "正在加载 YAML 内容...",
"create_node": "创建节点",
"copy_node": "复制节点",
"delete_node": "删除节点",
"delete_edge": "删除连线",
"workflow_graph": "工作流图",
"yaml_configuration": "YAML 配置",
"configure_graph": "配置图",
"launch": "启动",
"rename_workflow": "重命名工作流",
"copy_workflow": "复制工作流",
"manage_variables": "管理变量",
"manage_memories": "管理记忆",
"create_edge": "创建连线",
"enter_new_name": "输入新工作流名称",
"workflow_name": "工作流名称"
},
"batch_run": {
"title": "实验室",
"batch_settings": "批量设置",
"logs_placeholder": "批量处理日志将显示在此处...",
"rows_completed": "完成行数",
"total_time": "总耗时",
"success_rate": "成功率",
"current_status": "当前状态",
"overall_progress": "总体进度",
"workflow_selection": "选择工作流",
"loading": "加载中...",
"select_yaml": "选择 YAML 文件...",
"no_results": "无结果",
"input_file_selection": "选择输入文件",
"select_input_file": "选择输入文件...",
"input_file_format": "输入文件格式",
"view": "视图",
"dashboard": "仪表盘",
"terminal": "终端",
"max_parallel": "最大并发数",
"max_parallel_desc": "并行启动工作流的最大数量",
"log_level": "日志级别",
"log_level_desc": "日志详细级别",
"download_logs": "下载日志",
"status_completed": "批次已完成",
"status_cancelled": "批次已取消",
"status_in_progress": "进行中",
"status_idle": "空闲",
"status_waiting_workflow": "等待选择工作流...",
"status_waiting_file": "等待选择输入文件...",
"manual_intro": "输入文件应至少包含 task任务和/或 attachments附件列",
"manual_id": "必须唯一,若未找到对应列则自动生成",
"manual_task": "存储用户输入",
"manual_vars": "包含全局变量键值对的 JSON 对象",
"manual_attachments": "包含工作流附件绝对文件路径的 JSON 数组",
"launch": "启动",
"relaunch": "重新启动"
},
"launch": {
"title": "启动",
"collapse_chat": "折叠聊天",
"expand_chat": "展开聊天",
"loading_image": "加载图片...",
"preparing": "准备中...",
"upload_file": "上传文件",
"uploading": "上传中...",
"no_files_uploaded": "未上传文件",
"drop_files": "将文件拖放到此处",
"enter_prompt": "请输入任务提示词...",
"status": "状态",
"view": "视图",
"chat": "聊天",
"graph": "图",
"send": "发送",
"relaunch": "重新启动",
"download_logs": "下载日志",
"settings": "设置",
"download": "下载",
"workflow_selection": "工作流选择",
"loading": "加载中...",
"select_yaml": "选择 YAML 文件...",
"no_results": "无结果",
"status_waiting_workflow": "等待选择工作流...",
"status_connecting": "连接中...",
"status_waiting_launch": "等待启动...",
"status_running": "运行中...",
"status_completed": "已完成",
"status_cancelled": "已取消",
"status_failed": "失败",
"status_error": "错误",
"status_disconnected": "已断开连接",
"status_waiting_input": "等待输入...",
"status_launching": "启动中...",
"status_connection_error": "连接错误",
"launch_button": "启动",
"alert_session_not_ready": "会话尚未就绪。请等待连接。",
"alert_failed_upload": "文件上传失败",
"alert_file_upload_failed": "文件上传失败,请重试。",
"alert_recording_error": "录制发生错误",
"alert_mic_access_failed": "麦克风访问失败。请检查权限。",
"alert_recording_upload_failed": "录制上传失败,请重试。",
"alert_choose_workflow": "请选择一个工作流文件!",
"alert_ws_error": "WebSocket 连接错误!",
"alert_missing_session": "服务器缺少会话信息。",
"alert_enter_prompt": "请输入任务提示或上传文件。",
"alert_ws_not_ready": "WebSocket 连接尚未就绪。",
"alert_failed_launch": "启动工作流失败",
"unknown_error": "未知错误",
"alert_failed_execute": "调用 execute API 失败",
"alert_download_failed": "下载文件失败,请重试。",
"alert_download_logs_failed": "下载失败,请稍后重试",
"no_initial_instructions": "未提供初始说明",
"workflow_cancelled": "工作流已取消"
},
"components": {
"workflow_edge": {
"includes": "包含:{values}",
"excludes": "排除:{values}",
"regex": "正则表达式:{values}"
},
"start_node": {
"title": "起始节点"
},
"rich_tooltip": {
"learn_more": "了解更多 →"
},
"inline_config": {
"advanced_settings": "高级设置"
},
"collapsible_message": {
"copied": "已复制!",
"copy_original": "复制原始内容",
"show_more": "显示更多",
"show_less": "显示更少"
}
},
"form_generator": {
"advanced_settings": "高级设置",
"copy_node": "复制节点",
"submit": "提交",
"key": "键",
"value": "值",
"cancel": "取消",
"confirm": "确认",
"edit_entry": "编辑条目",
"add_entry": "添加条目",
"delete_node": "删除节点",
"delete_edge": "删除边",
"delete": "删除",
"field_required": "\"{field}\" 为必填项",
"failed_to_load_yaml": "加载现有工作流 YAML 失败",
"graph_id_required": "保存工作流需要 Graph ID",
"no_workflow_context": "删除时无可用工作流上下文",
"node_id_required": "删除时需要 Node ID",
"failed_to_save": "保存工作流失败",
"failed_to_delete": "删除条目失败",
"failed_to_generate_yaml": "生成 YAML 输出失败",
"failed_to_submit": "提交表单失败,错误:\n",
"failed_to_load_child": "加载子 Schema 失败,错误:\n",
"key_required": "键为必填项",
"value_required": "值为必填项",
"invalid_field_context": "字段上下文无效",
"key_exists": "键已存在",
"confirm_delete": "确定要删除此 {entity} 吗?",
"entry_configured_single": "{label}: 配置了 1 个条目",
"entry_configured_multiple": "{label}: 配置了 {count} 个条目",
"configured": "{label}: 已配置",
"invalid_configuration": "配置无效",
"error_in": "{key} 中的错误:{error}"
},
"dynamic_form_field": {
"type_to_filter": "输入以筛选选项...",
"none": "无",
"no_options_found": "未找到选项",
"add_key_value": "添加键值对",
"add_entry": "添加条目",
"select_type_and_configure": "请选择类型并进行配置",
"clear_configuration": "清除配置",
"delete_child": "删除子项",
"delete_variable": "删除变量",
"delete_item": "删除项目",
"edit_child": "编辑 {child}",
"configure_child": "配置 {child}",
"configure": "配置"
},
"schema": {
"agent_role": "Agent 角色",
"model": "模型",
"temperature": "温度",
"max_tokens": "最大 Token 数",
"top_p": "Top P",
"presence_penalty": "存在惩罚",
"frequency_penalty": "频率惩罚",
"prompt": "提示词",
"description": "描述",
"graph": "图谱",
"nodes": "节点",
"edges": "边",
"start": "开始",
"end": "结束",
"vars": "变量",
"memory": "记忆",
"id": "ID",
"name": "名称",
"type": "类型",
"config": "配置",
"condition": "条件",
"trigger": "触发器",
"from": "来源",
"to": "目标",
"version": "版本",
"is_majority_voting": "多数投票",
"log_level": "日志级别",
"context_window": "上下文窗口",
"provider": "提供商",
"base_url": "基础 URL",
"api_key": "API 密钥",
"params": "参数",
"tooling": "工具集",
"tools": "工具",
"auto_fill": "自动填充",
"timeout": "超时时间",
"server": "服务器",
"headers": "请求头",
"command": "命令",
"args": "参数",
"cwd": "当前工作目录 (CWD)",
"env": "环境变量",
"inherit_env": "继承环境",
"startup_timeout": "启动超时时间",
"wait_for_log": "等待日志",
"thinking": "思考",
"reflection_prompt": "反思提示词",
"memories": "记忆",
"retrieve_stage": "检索阶段",
"top_k": "Top K",
"similarity_threshold": "相似度阈值",
"read": "读取",
"write": "写入",
"retry": "重试",
"enabled": "启用",
"max_attempts": "最大尝试次数",
"min_wait_seconds": "最小等待秒数",
"max_wait_seconds": "最大等待秒数",
"retry_on_status_codes": "基于状态码的重试",
"retry_on_exception_types": "基于异常类型的重试",
"non_retry_exception_types": "非重试异常类型",
"retry_on_error_substrings": "基于错误子串的重试",
"initial_instruction": "初始指令",
"organization": "组织"
},
"schema_desc": {
"agent_role": "定义智能体的系统级角色、身份或行为准则。",
"model": "指定要使用的底层大语言模型配置 (例如 gpt-4, gpt-3.5-turbo)。",
"temperature": "控制生成结果的随机性或创造性。较高的值产生更多随机输出。",
"max_tokens": "单次响应生成的最大 Token 数量限制。",
"top_p": "考虑标记概率质量的累计分布。推荐在改变温度范围时保持不变。",
"presence_penalty": "基于新词是否出现在目前生成的文本中来惩罚它们,增加谈论新话题的可能性。",
"frequency_penalty": "基于新词在目前生成的文本中的频率来惩罚它们,减少模型原样重复相同内容的可能性。",
"prompt": "在此输入针对该节点执行任务的特定提示词或详细指令。",
"description": "以人类可读的文字详细说明当前工作流、节点或配置的功能目标。",
"graph": "整个工作网状图的底层定义和配置集合。",
"nodes": "此工作流包含的一系列处理节点单元。",
"edges": "定义节点之间如何连接和按顺序传递数据的连线集合。",
"start": "指定哪些节点ID作为当前工作流或子图的起始点。",
"end": "指定哪些节点ID作为终点。用于收集图表的最终输出。",
"vars": "设定可被工作流节点引用的全局变量池。",
"memory": "配置能提供给模型作为长期挂载记忆的外部存储配置。",
"id": "用于唯一标识和引用的标识符。只能包含字母、数字、下划线或连字符,且不能有空格。",
"name": "用于展示用的简短名称标签。",
"type": "定义当前节点或组件的运行类型机制。",
"config": "当前节点的详细参数设置池。",
"condition": "设定只有当本字段运行结果为真时,数据才能顺着当前条件连线流向下一节点。",
"trigger": "指定连线是否可以触发激活下游节点的执行。如果不触发,则仅作为数据通道。",
"from": "连线的起点数据来源。",
"to": "连线的终点数据接收方。",
"version": "当前配置结构的版本号标记。",
"is_majority_voting": "决定此工作图是否采用多分支并发执行并在最终输出前做多数票决机制。",
"log_level": "选择运行时输出日志的详细信息程度。",
"context_window": "限制模型在一次处理中所能读取的最大历史上下文标记数。",
"provider": "指定大语言模型的 API 提供商 (如 OpenAI, Anthropic等)。",
"base_url": "如果使用了定制化或本地的代理转发,可修改覆盖模型默认基础 URL。",
"api_key": "用于验证调用该模型服务的 API 密钥授权码。",
"params": "附加传输给底层模型服务的可选任意参数字典。",
"tooling": "启用并定义允许模型主动调用的外部函数或工具列表。",
"tools": "列出可被使用的可用外部工具。",
"auto_fill": "开启自动填充功能时,系统会自动补全必填参数片段。",
"timeout": "在此节点调用服务等待超时的最大容忍时间。",
"server": "网络服务的目标服务器地址等相关参数。",
"headers": "发送网络请求时需要附带的自定义表头信息。",
"command": "需要在系统终端中执行的具体命令。",
"args": "启动该系统命令时额外传入的各组参数。",
"cwd": "设定执行代码或命令时的运行目录,所有相对路径将默认此目录。",
"env": "可覆盖并配置的专属系统环境变量。",
"inherit_env": "是否继承当前宿主系统的全部环境变量参数。",
"startup_timeout": "设定等待命令初始化或服务完全启动的最大容限时长。",
"wait_for_log": "通过监听并等待指定的日志行特征来确认前置服务就绪。",
"thinking": "开启让模型展现逐步自我反省与内部思考过程的功能。",
"reflection_prompt": "覆盖或增加用作引导模型自我复审与反思纠错的特殊配置。",
"memories": "链接到外部数据记忆存储区域,支持长期长效的查询能力。",
"retrieve_stage": "指示检索引擎所应用的检索环节阶段(如预加载或是被动查询等)。",
"top_k": "在检索相似上下文片段时最多返回的文档区块数量。",
"similarity_threshold": "指定被接受且纳入考虑范围内的匹配数据最低相似度分级。",
"read": "开启或控制系统读取外部缓存能力设定。",
"write": "开启控制系统向外部持久层写回数据的设定。",
"retry": "统一设定应对临时性错误的重试配置。",
"enabled": "指明当前特性或节点功能是否处于激活状态。",
"max_attempts": "限定尝试失败的最大可重试次数上限。",
"min_wait_seconds": "配置失败发起重试前最少退避时长(秒)。",
"max_wait_seconds": "配置重试间隔时长递增时的最大等待时长(秒)。",
"retry_on_status_codes": "只遇到特定网络状态码时才激活重试。",
"retry_on_exception_types": "遇指定代码异常类型时才重试。",
"non_retry_exception_types": "设定不进行徒劳重试的限制类型。",
"retry_on_error_substrings": "利用关键字正则匹配错误触发重试。",
"initial_instruction": "系统维度的初始上下文统领级全局指令声明。"
},
"help": {
"startNode": {
"title": "起始节点",
"description": "您的工作流的入口点。当工作流启动时,所有连接到起始节点的节点将同时并行运行。",
"examples": [
"连接多个节点以同时启动它们",
"首先执行的节点会接收您的初始输入内容"
]
},
"workflowNode": {
"agent": {
"title": "Agent 节点 (智能体)",
"description": "一个能够推理、生成内容以及使用工具的 AI 智能体。智能体接收消息并根据其配置产出相应的响应内容。",
"examples": [
"内容生成 (写作、编码、分析)",
"逻辑决策与路由分发",
"使用外部工具 (搜索、文件操作、API 调用)"
]
},
"human": {
"title": "Human 节点 (人工节点)",
"description": "暂停工作流执行并等待人工输入介入。使用此节点来审查内容、做决策或提供反馈修订。",
"examples": [
"审查并批准 AI 生成的内容",
"提供额外的修正指令",
"在不同的工作流分支中做选择"
]
},
"python": {
"title": "Python 节点",
"description": "在您的本地环境中执行 Python 代码。代码将在工作区目录下运行,并可以访问用户上传的文件。",
"examples": [
"数据处理和分析清洗",
"运行由 AI 生成的代码测试",
"直接在本地操作文件"
]
},
"passthrough": {
"title": "Passthrough 节点 (透传节点)",
"description": "将接收到的消息未经任何修改的地传递给下一个节点。常用于工作流结构的整理和在循环执行中过滤无用输出。",
"examples": [
"在循环体内保留初始的上游上下文",
"过滤掉多余冗余的节点输出",
"将工作流结构的连线梳理得更整洁"
]
},
"literal": {
"title": "Literal 节点 (常量节点)",
"description": "直接输出固定的文本内容,忽略一切上游输入。可以用来在工作流的特定位置硬性植入指令或上下文。",
"examples": [
"在目标节点前强制追加固定的系统设定",
"注入硬编码的上下文或约束条件",
"提供模拟的测试样例数据"
]
},
"loop_counter": {
"title": "Loop Counter 节点 (循环计数器)",
"description": "用来限制循环的迭代次数。仅仅在达到最大的计数值时才会产生输出,这在使用 AI 时能有效防止无限死循环。",
"examples": [
"防止工作流陷入失控的失控循环",
"设定最大允许的工作流修改/审查次数",
"控制复杂的迭代处理流程"
]
},
"subgraph": {
"title": "Subgraph 节点 (子图节点)",
"description": "将另外一个独立的工作流文件作为可重用模块嵌入进来。有了它即可实现模块化设计与大型工作流协同组合。",
"examples": [
"在不同的工作流间重用常用的功能模式",
"将巨大复杂的工作流拆分成多个方便管理的小块",
"在不同的团队间共享固定的工作流资产"
]
},
"unknown": {
"title": "工作流节点",
"description": "您工作流中的一个节点。点击即可查看并编辑其相关配置属性。"
}
},
"edge": {
"basic": {
"title": "节点连线",
"description": "用来连接两个节点以控制信息数据流向和逻辑执行顺序。上游节点输出的结果将直接作为下游节点的输入内容。",
"examples": [
"数据从源节点无缝传递到目标节点",
"等待源节点完全处理结束后目标节点再开始执行"
]
},
"trigger": {
"enabled": {
"description": "当前连线会触发并唤醒处于下游的节点执行。"
},
"disabled": {
"description": "目前连线仅会传递数据,但不触发下游执行。这说明下游节点只能在被其它节点触发时才会运行。"
}
},
"condition": {
"hasCondition": {
"description": "这是一个附带条件的连线。只有当满足相关条件并计算为 true 时连线上的数据流才会激活穿行。"
}
}
},
"contextMenu": {
"createNode": {
"description": "在您的工作流中创建一个全新的节点。可以在智能体 (Agent)、人工 (Human)、代码 (Python) 等各个节点类型中选择。"
},
"copyNode": {
"description": "带着原有的全部设置完美复刻这个节点。由于是克隆产生的,所以新生成的节点需要您手动给它重置一个空白的节点 ID。"
},
"deleteNode": {
"description": "从当前工作流内彻底移除该节点及其所连接的所有上下游连线。"
},
"deleteEdge": {
"description": "移除这两个节点之间的连线与关系。"
},
"createNodeButton": {
"description": "打开节点创建表单。您也可以在画布空白处右键点击来将节点直接创建在该对应的坐标位置上。"
},
"configureGraph": {
"description": "配置整个工作流级别的全局设定,例如工作流最终名称、文字描述以及那些关键的全局变量参数。"
},
"launch": {
"description": "使用任务提示词直接运行您的这个工作流。工作流将会自动执行并且即刻为您展示中间的所有思考运行轨迹与最终产出结果。"
},
"createEdge": {
"description": "在两个节点模型中创建连接桥梁。当然,您更可以直接通过在节点的手柄圆点上拖拽出一条线来直观地构建连线图谱。"
},
"manageVariables": {
"description": "统一定义全局共享变量 (比如各种 API key 或默认参数),这样整个工作流所有的节点都能通用 ${变量名} 这一语法来提取并访问利用它们。"
},
"manageMemories": {
"description": "跨越多次工作流执行,配置长期的工作记忆模块专门用作存储信息并支持快速的知识检索反馈。"
},
"renameWorkflow": {
"description": "重新修改并调整此工作流配置文件的名称。"
},
"copyWorkflow": {
"description": "为此完整的工作流模型资产创建一个同结构副本,它将被赋予一个新的副本名称存储到同目录下。"
}
}
}
}

View File

@ -2,6 +2,7 @@ import { createApp } from 'vue'
import './style.css' import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import i18n from './i18n'
import './assets/styles/fonts.css' import './assets/styles/fonts.css'
createApp(App).use(router).mount('#app') createApp(App).use(router).use(i18n).mount('#app')

View File

@ -2,8 +2,8 @@
<div class="launch-view"> <div class="launch-view">
<div class="launch-bg"></div> <div class="launch-bg"></div>
<div class="header"> <div class="header">
<h1>Labaratory</h1> <h1>{{ $t('batch_run.title') }}</h1>
<button class="settings-button" @click="showBatchSettingsModal()" title="Batch Settings"> <button class="settings-button" @click="showBatchSettingsModal()" :title="$t('batch_run.batch_settings')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle> <circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
@ -20,7 +20,7 @@
<span :class="`log-timestamp log-timestamp-${logMessage.type}`">{{ logMessage.timestamp }}</span> : {{ logMessage.message }} <span :class="`log-timestamp log-timestamp-${logMessage.type}`">{{ logMessage.timestamp }}</span> : {{ logMessage.message }}
</div> </div>
<div v-if="logMessages.length === 0" class="log-placeholder"> <div v-if="logMessages.length === 0" class="log-placeholder">
Batch processing logs will appear here... {{ $t('batch_run.logs_placeholder') }}
</div> </div>
</div> </div>
</div> </div>
@ -31,26 +31,26 @@
<!-- Metrics Grid --> <!-- Metrics Grid -->
<div class="metrics-grid"> <div class="metrics-grid">
<div class="metric-card" :class="{ 'status-active': status === 'In Progress' }"> <div class="metric-card" :class="{ 'status-active': status === 'In Progress' }">
<div class="metric-title">Rows Completed</div> <div class="metric-title">{{ $t('batch_run.rows_completed') }}</div>
<div class="metric-value">{{ completedRows }}</div> <div class="metric-value">{{ completedRows }}</div>
</div> </div>
<div class="metric-card" :class="{ 'status-active': status === 'In Progress' }"> <div class="metric-card" :class="{ 'status-active': status === 'In Progress' }">
<div class="metric-title">Total Time</div> <div class="metric-title">{{ $t('batch_run.total_time') }}</div>
<div class="metric-value">{{ totalTime }}</div> <div class="metric-value">{{ totalTime }}</div>
</div> </div>
<div class="metric-card" :class="{ 'status-active': status === 'In Progress' }"> <div class="metric-card" :class="{ 'status-active': status === 'In Progress' }">
<div class="metric-title">Success Rate</div> <div class="metric-title">{{ $t('batch_run.success_rate') }}</div>
<div class="metric-value">{{ successRate }}</div> <div class="metric-value">{{ successRate }}</div>
</div> </div>
<div class="metric-card" :class="{ 'status-active': status === 'In Progress' }"> <div class="metric-card" :class="{ 'status-active': status === 'In Progress' }">
<div class="metric-title">Current Status</div> <div class="metric-title">{{ $t('batch_run.current_status') }}</div>
<div class="metric-value" :class="{ 'status-active': status === 'In Progress' }">{{ computedStatus }}</div> <div class="metric-value" :class="{ 'status-active': status === 'In Progress' }">{{ computedStatus }}</div>
</div> </div>
</div> </div>
<!-- Progress Bar --> <!-- Progress Bar -->
<div class="progress-section"> <div class="progress-section">
<div class="progress-label">Overall Progress</div> <div class="progress-label">{{ $t('batch_run.overall_progress') }}</div>
<div class="progress-bar" :style="{ '--process-width': progressPercentage + '%'}"> <div class="progress-bar" :style="{ '--process-width': progressPercentage + '%'}">
<div class="progress-fill" :class="{ 'processing': status === 'In Progress' }" :style="{ width: progressPercentage + '%' }"></div> <div class="progress-fill" :class="{ 'processing': status === 'In Progress' }" :style="{ width: progressPercentage + '%' }"></div>
</div> </div>
@ -63,7 +63,7 @@
<!-- Right panel --> <!-- Right panel -->
<div class="right-panel"> <div class="right-panel">
<div class="control-section"> <div class="control-section">
<label class="section-label">Workflow Selection</label> <label class="section-label">{{ $t('batch_run.workflow_selection') }}</label>
<div <div
class="select-wrapper custom-file-selector" class="select-wrapper custom-file-selector"
ref="fileSelectorWrapperRef" ref="fileSelectorWrapperRef"
@ -73,7 +73,7 @@
v-model="fileSearchQuery" v-model="fileSearchQuery"
type="text" type="text"
class="file-selector-input" class="file-selector-input"
:placeholder="loading ? 'Loading...' : 'Select YAML file...'" :placeholder="loading ? $t('batch_run.loading') : $t('batch_run.select_yaml')"
:disabled="loading || isWorkflowRunning" :disabled="loading || isWorkflowRunning"
@focus="handleFileInputFocus" @focus="handleFileInputFocus"
@input="handleFileInputChange" @input="handleFileInputChange"
@ -104,13 +104,13 @@
v-if="!filteredWorkflowFiles.length" v-if="!filteredWorkflowFiles.length"
class="file-empty" class="file-empty"
> >
No results {{ $t('batch_run.no_results') }}
</li> </li>
</ul> </ul>
</Transition> </Transition>
</div> </div>
<label class="section-label">Input File Selection</label> <label class="section-label">{{ $t('batch_run.input_file_selection') }}</label>
<div class="input-file-section"> <div class="input-file-section">
<div class="file-upload-wrapper"> <div class="file-upload-wrapper">
<input <input
@ -126,14 +126,14 @@
:disabled="loading || isWorkflowRunning" :disabled="loading || isWorkflowRunning"
@click="handleInputFileButtonClick" @click="handleInputFileButtonClick"
> >
{{ selectedInputFile ? selectedInputFile.name : 'Select input file...' }} {{ selectedInputFile ? selectedInputFile.name : $t('batch_run.select_input_file') }}
</button> </button>
</div> </div>
</div> </div>
<div class="input-manual"> <div class="input-manual">
<div class="manual-title" @click="showColumnGuideModal = true"> <div class="manual-title" @click="showColumnGuideModal = true">
Input File Format {{ $t('batch_run.input_file_format') }}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="info-icon"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="info-icon">
<circle cx="12" cy="12" r="10"></circle> <circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path> <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
@ -142,21 +142,21 @@
</div> </div>
</div> </div>
<label class="section-label">View</label> <label class="section-label">{{ $t('batch_run.view') }}</label>
<div class="view-toggle"> <div class="view-toggle">
<button <button
class="toggle-button" class="toggle-button"
:class="{ active: viewMode === 'dashboard' }" :class="{ active: viewMode === 'dashboard' }"
@click="switchToDashboard" @click="switchToDashboard"
> >
Dashboard {{ $t('batch_run.dashboard') }}
</button> </button>
<button <button
class="toggle-button" class="toggle-button"
:class="{ active: viewMode === 'terminal' }" :class="{ active: viewMode === 'terminal' }"
@click="viewMode = 'terminal'" @click="viewMode = 'terminal'"
> >
Terminal {{ $t('batch_run.terminal') }}
</button> </button>
</div> </div>
@ -177,7 +177,7 @@
:disabled="status !== 'In Progress'" :disabled="status !== 'In Progress'"
@click="cancelBatchWorkflow" @click="cancelBatchWorkflow"
> >
Cancel {{ $t('common.cancel') }}
</button> </button>
<button <button
@ -185,7 +185,7 @@
:disabled="status !== 'Batch completed' && status !== 'Batch cancelled'" :disabled="status !== 'Batch completed' && status !== 'Batch cancelled'"
@click="downloadLogs" @click="downloadLogs"
> >
Download Logs {{ $t('batch_run.download_logs') }}
</button> </button>
</div> </div>
</div> </div>
@ -225,16 +225,16 @@
<div class="modal-content"> <div class="modal-content">
<div class="manual-content"> <div class="manual-content">
<div class="manual-item"> <div class="manual-item">
Input file should contain at least <code>task</code> and/or <code>attachments</code> columns {{ $t('batch_run.manual_intro') }}
</div> </div>
<div class="manual-item"> <div class="manual-item">
<code>id</code> - Must be unique, auto-generated if column not found <code>id</code> - {{ $t('batch_run.manual_id') }}
</div> </div>
<div class="manual-item"> <div class="manual-item">
<code>task</code> - Holds user input <code>task</code> - {{ $t('batch_run.manual_task') }}
</div> </div>
<div class="manual-item"> <div class="manual-item">
<code>vars</code> - JSON object containing key-value pairs of global variables <code>vars</code> - {{ $t('batch_run.manual_vars') }}
<div class="manual-example"> <div class="manual-example">
<pre>{{ <pre>{{
JSON.stringify({"BASE_URL": "openai.com","API_KEY": "123"}, null, 2) JSON.stringify({"BASE_URL": "openai.com","API_KEY": "123"}, null, 2)
@ -242,7 +242,7 @@ JSON.stringify({"BASE_URL": "openai.com","API_KEY": "123"}, null, 2)
</div> </div>
</div> </div>
<div class="manual-item"> <div class="manual-item">
<code>attachments</code> - JSON array containing absolute file paths of attachments for workflow <code>attachments</code> - {{ $t('batch_run.manual_attachments') }}
<div class="manual-example"> <div class="manual-example">
<pre>{{ <pre>{{
JSON.stringify(["C:\\a_sheep.png"], null, 2) JSON.stringify(["C:\\a_sheep.png"], null, 2)
@ -265,12 +265,12 @@ JSON.stringify(["C:\\a_sheep.png"], null, 2)
<div class="batch-settings-modal"> <div class="batch-settings-modal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3>Batch Settings</h3> <h3>{{ $t('batch_run.batch_settings') }}</h3>
<button class="close-button" @click="isBatchSettingsModalVisible = false">×</button> <button class="close-button" @click="isBatchSettingsModalVisible = false">×</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="settings-item"> <div class="settings-item">
<label class="setting-label">Max. Parallel Launches</label> <label class="setting-label">{{ $t('batch_run.max_parallel') }}</label>
<input <input
type="number" type="number"
v-model.number="maxParallel" v-model.number="maxParallel"
@ -279,21 +279,21 @@ JSON.stringify(["C:\\a_sheep.png"], null, 2)
max="50" max="50"
step="1" step="1"
/> />
<p class="setting-desc">Maximum number of parallel workflow launches</p> <p class="setting-desc">{{ $t('batch_run.max_parallel_desc') }}</p>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<label class="setting-label">Log Level</label> <label class="setting-label">{{ $t('batch_run.log_level') }}</label>
<select v-model="logLevel" class="setting-select"> <select v-model="logLevel" class="setting-select">
<option v-for="level in logLevelOptions" :key="level" :value="level"> <option v-for="level in logLevelOptions" :key="level" :value="level">
{{ level }} {{ level }}
</option> </option>
</select> </select>
<p class="setting-desc">Logging verbosity level</p> <p class="setting-desc">{{ $t('batch_run.log_level_desc') }}</p>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="cancel-button" @click="isBatchSettingsModalVisible = false">Cancel</button> <button class="cancel-button" @click="isBatchSettingsModalVisible = false">{{ $t('common.cancel') }}</button>
<button class="confirm-button" @click="isBatchSettingsModalVisible = false">Save</button> <button class="confirm-button" @click="isBatchSettingsModalVisible = false">{{ $t('common.save') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -304,6 +304,8 @@ JSON.stringify(["C:\\a_sheep.png"], null, 2)
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
import { fetchWorkflowsWithDesc, fetchLogsZip, fetchWorkflowYAML, postFile, getAttachment, fetchVueGraph, postBatchWorkflow } from '../utils/apiFunctions.js' import { fetchWorkflowsWithDesc, fetchLogsZip, fetchWorkflowYAML, postFile, getAttachment, fetchVueGraph, postBatchWorkflow } from '../utils/apiFunctions.js'
import { configStore } from '../utils/configStore.js' import { configStore } from '../utils/configStore.js'
import { spriteFetcher } from '../utils/spriteFetcher.js' import { spriteFetcher } from '../utils/spriteFetcher.js'
@ -350,6 +352,7 @@ import CollapsibleMessage from '../components/CollapsibleMessage.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { fromObject, fitView, onPaneReady, onNodesInitialized, setNodes, setEdges, nodes, edges } = useVueFlow()
// Log box state // Log box state
const logMessages = ref([]) const logMessages = ref([])
@ -380,10 +383,17 @@ const loading = ref(false)
// Computed status from workflow and input file selection // Computed status from workflow and input file selection
const computedStatus = computed(() => { const computedStatus = computed(() => {
if (loading.value) return 'Loading...' if (loading.value) return t('batch_run.loading')
if (status.value === 'Pending workflow selection') return status.value if (status.value === 'Pending workflow selection') return t('batch_run.status_waiting_workflow')
if (!selectedYamlFile.value) return 'Pending workflow selection' if (!selectedYamlFile.value) return t('batch_run.status_waiting_workflow')
if (!selectedInputFile.value) return 'Pending file selection' if (!selectedInputFile.value) return t('batch_run.status_waiting_file')
// Handle specific statuses from logic
if (status.value === 'Idle') return t('batch_run.status_idle')
if (status.value === 'In Progress') return t('batch_run.status_in_progress')
if (status.value === 'Batch completed') return t('batch_run.status_completed')
if (status.value === 'Batch cancelled') return t('batch_run.status_cancelled')
return status.value return status.value
}) })
@ -504,9 +514,9 @@ const filteredWorkflowFiles = computed(() => {
// Button label computed property // Button label computed property
const buttonLabel = computed(() => { const buttonLabel = computed(() => {
if (status.value === 'Batch completed' || status.value === 'Batch cancelled') { if (status.value === 'Batch completed' || status.value === 'Batch cancelled') {
return 'Relaunch' return t('batch_run.relaunch')
} }
return 'Launch' return t('batch_run.launch')
}) })
// Computed button state // Computed button state

View File

@ -48,17 +48,17 @@ const cubes = Array.from({ length: 80 }, (_, i) => ({
</div> </div>
<div class="content-wrapper"> <div class="content-wrapper">
<h1 class="title"> <h1 class="title">
<span class="title-line">ChatDev 2.0</span> <span class="title-line">{{ $t('home.title') }}</span>
<span class="title-line title-highlight">DevAll</span> <span class="title-line title-highlight">{{ $t('home.highlight') }}</span>
</h1> </h1>
<p class="introduction"> <p class="introduction">
ChatDev 2.0 - DevAll is a zero-code multi-agent platform for developing everything, with a workspace built for designing, visualizing, and running agent workflows. {{ $t('home.introduction') }}
</p> </p>
<div class="actions"> <div class="actions">
<button class="btn get-started-btn" @click="goToTutorial"> <button class="btn get-started-btn" @click="goToTutorial">
Get Started {{ $t('home.get_started') }}
</button> </button>
</div> </div>
</div> </div>
@ -299,7 +299,7 @@ const cubes = Array.from({ length: 80 }, (_, i) => ({
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.title { .title {
font-size: 42px; font-size: 42px;
} }

View File

@ -2,8 +2,8 @@
<div class="launch-view"> <div class="launch-view">
<div class="launch-bg"></div> <div class="launch-bg"></div>
<div class="header"> <div class="header">
<h1>Launch</h1> <h1>{{ $t('launch.title') }}</h1>
<button class="settings-button" @click="showSettingsModal = true" title="Settings"> <button class="settings-button" @click="showSettingsModal = true" :title="$t('launch.settings')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle> <circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
@ -22,7 +22,7 @@
}" }"
v-show="viewMode === 'chat' || true" v-show="viewMode === 'chat' || true"
> >
<button v-show="viewMode !== 'chat'" class="chat-panel-toggle" @click="isChatPanelOpen = !isChatPanelOpen" :title="isChatPanelOpen ? 'Collapse chat' : 'Expand chat'"> <button v-show="viewMode !== 'chat'" class="chat-panel-toggle" @click="isChatPanelOpen = !isChatPanelOpen" :title="isChatPanelOpen ? $t('launch.collapse_chat') : $t('launch.expand_chat')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :class="{ 'chevron-collapsed': !isChatPanelOpen }"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :class="{ 'chevron-collapsed': !isChatPanelOpen }">
<polyline points="15 18 9 12 15 6"></polyline> <polyline points="15 18 9 12 15 6"></polyline>
</svg> </svg>
@ -107,7 +107,7 @@
v-if="message.loading" v-if="message.loading"
class="artifact-status" class="artifact-status"
> >
Loading image... {{ $t('launch.loading_image') }}
</div> </div>
<div <div
v-else-if="message.error" v-else-if="message.error"
@ -142,7 +142,7 @@
:disabled="message.loading" :disabled="message.loading"
@click="downloadArtifact(message)" @click="downloadArtifact(message)"
> >
Download {{ $t('launch.download') }}
</button> </button>
</div> </div>
</div> </div>
@ -167,7 +167,7 @@
:disabled="message.loading" :disabled="message.loading"
@click="downloadArtifact(message)" @click="downloadArtifact(message)"
> >
{{ message.loading ? 'Preparing...' : 'Download' }} {{ message.loading ? $t('launch.preparing') : $t('launch.download') }}
</button> </button>
</div> </div>
@ -196,7 +196,7 @@
v-model="taskPrompt" v-model="taskPrompt"
class="task-input" class="task-input"
:disabled="!isConnectionReady || (isWorkflowRunning && status !== 'Waiting for input...')" :disabled="!isConnectionReady || (isWorkflowRunning && status !== 'Waiting for input...')"
placeholder="Please enter task prompt..." :placeholder="$t('launch.enter_prompt')"
ref="taskInputRef" ref="taskInputRef"
@keydown.enter="handleEnterKey" @keydown.enter="handleEnterKey"
@paste="handlePaste" @paste="handlePaste"
@ -215,7 +215,7 @@
:disabled="!isConnectionReady || !sessionId || isUploadingAttachment || (isWorkflowRunning && status !== 'Waiting for input...')" :disabled="!isConnectionReady || !sessionId || isUploadingAttachment || (isWorkflowRunning && status !== 'Waiting for input...')"
@click="handleAttachmentButtonClick" @click="handleAttachmentButtonClick"
> >
{{ isUploadingAttachment ? 'Uploading...' : 'Upload File' }} {{ isUploadingAttachment ? $t('launch.uploading') : $t('launch.upload_file') }}
</button> </button>
<span <span
v-if="uploadedAttachments.length" v-if="uploadedAttachments.length"
@ -255,7 +255,7 @@
v-if="!uploadedAttachments.length" v-if="!uploadedAttachments.length"
class="attachment-empty" class="attachment-empty"
> >
No files uploaded {{ $t('launch.no_files_uploaded') }}
</div> </div>
</div> </div>
</Transition> </Transition>
@ -300,7 +300,7 @@
</div> </div>
</div> </div>
<div v-if="isDragActive" class="drag-overlay"> <div v-if="isDragActive" class="drag-overlay">
<div class="drag-overlay-content">Drop files to upload</div> <div class="drag-overlay-content">{{ $t('launch.drop_files') }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -349,7 +349,7 @@
<!-- Right panel --> <!-- Right panel -->
<div class="right-panel"> <div class="right-panel">
<div class="control-section"> <div class="control-section">
<label class="section-label">Workflow Selection</label> <label class="section-label">{{ $t('launch.workflow_selection') }}</label>
<div <div
class="select-wrapper custom-file-selector" class="select-wrapper custom-file-selector"
ref="fileSelectorWrapperRef" ref="fileSelectorWrapperRef"
@ -359,7 +359,7 @@
v-model="fileSearchQuery" v-model="fileSearchQuery"
type="text" type="text"
class="file-selector-input" class="file-selector-input"
:placeholder="loading ? 'Loading...' : 'Select YAML file...'" :placeholder="loading ? $t('launch.loading') : $t('launch.select_yaml')"
:disabled="loading || isWorkflowRunning" :disabled="loading || isWorkflowRunning"
@focus="handleFileInputFocus" @focus="handleFileInputFocus"
@input="handleFileInputChange" @input="handleFileInputChange"
@ -390,32 +390,32 @@
v-if="!filteredWorkflowFiles.length" v-if="!filteredWorkflowFiles.length"
class="file-empty" class="file-empty"
> >
No results {{ $t('launch.no_results') }}
</li> </li>
</ul> </ul>
</Transition> </Transition>
</div> </div>
<label class="section-label">Status</label> <label class="section-label">{{ $t('launch.status') }}</label>
<div class="status-display" :class="{ 'status-active': status === 'Running...' }"> <div class="status-display" :class="{ 'status-active': status === 'Running...' }">
{{ status }} {{ getTranslatedStatus(status) }}
</div> </div>
<label class="section-label">View</label> <label class="section-label">{{ $t('launch.view') }}</label>
<div class="view-toggle"> <div class="view-toggle">
<button <button
class="toggle-button" class="toggle-button"
:class="{ active: viewMode === 'chat' }" :class="{ active: viewMode === 'chat' }"
@click="viewMode = 'chat'" @click="viewMode = 'chat'"
> >
Chat {{ $t('launch.chat') }}
</button> </button>
<button <button
class="toggle-button" class="toggle-button"
:class="{ active: viewMode === 'graph' }" :class="{ active: viewMode === 'graph' }"
@click="switchToGraph" @click="switchToGraph"
> >
Graph {{ $t('launch.graph') }}
</button> </button>
</div> </div>
@ -434,7 +434,7 @@
:disabled="status !== 'Running...'" :disabled="status !== 'Running...'"
@click="cancelWorkflow" @click="cancelWorkflow"
> >
Cancel {{ $t('common.cancel') }}
</button> </button>
<button <button
@ -442,7 +442,7 @@
:disabled="status !== 'Completed' && status !== 'Cancelled'" :disabled="status !== 'Completed' && status !== 'Cancelled'"
@click="downloadLogs" @click="downloadLogs"
> >
Download Logs {{ $t('launch.download_logs') }}
</button> </button>
</div> </div>
</div> </div>
@ -475,6 +475,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { fetchWorkflowsWithDesc, fetchLogsZip, fetchWorkflowYAML, postFile, getAttachment, fetchVueGraph } from '../utils/apiFunctions.js' import { fetchWorkflowsWithDesc, fetchLogsZip, fetchWorkflowYAML, postFile, getAttachment, fetchVueGraph } from '../utils/apiFunctions.js'
import { configStore } from '../utils/configStore.js' import { configStore } from '../utils/configStore.js'
import { spriteFetcher } from '../utils/spriteFetcher.js' import { spriteFetcher } from '../utils/spriteFetcher.js'
@ -509,6 +510,29 @@ import CollapsibleMessage from '../components/CollapsibleMessage.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { t } = useI18n()
const { fromObject, fitView, onPaneReady, onNodesInitialized, setNodes, setEdges, nodes, edges } = useVueFlow()
const getTranslatedStatus = (statusText) => {
if (!statusText) return ''
const statusMap = {
'Waiting for workflow selection...': t('launch.status_waiting_workflow'),
'Connecting...': t('launch.status_connecting'),
'Connected': t('launch.status_connected'),
'Connection error': t('launch.status_connection_error'),
'Disconnected': t('launch.status_disconnected'),
'Pending launch': t('launch.status_pending_launch'),
'Launching...': t('launch.status_launching'),
'In Progress': t('launch.status_in_progress'),
'Completed': t('launch.status_completed'),
'Cancelled': t('launch.status_cancelled'),
'Failed': t('launch.status_failed'),
'Error': t('launch.status_error'),
'Pending workflow selection': t('launch.status_waiting_workflow'),
'Pending file selection': t('launch.status_waiting_file')
}
return statusMap[statusText] || statusText
}
// Task input state // Task input state
const taskPrompt = ref('') const taskPrompt = ref('')
@ -718,12 +742,12 @@ const filteredWorkflowFiles = computed(() => {
// Button label computed property // Button label computed property
const buttonLabel = computed(() => { const buttonLabel = computed(() => {
if (isWorkflowRunning.value) { if (isWorkflowRunning.value) {
return 'Send' return t('launch.send')
} }
if (status.value === 'Completed' || status.value === 'Cancelled') { if (status.value === 'Completed' || status.value === 'Cancelled') {
return 'Relaunch' return t('launch.relaunch')
} }
return 'Launch' return t('launch.launch_button')
}) })
const clearUploadedAttachments = () => { const clearUploadedAttachments = () => {
@ -968,7 +992,7 @@ const uploadFiles = async (files) => {
} }
if (!sessionId) { if (!sessionId) {
alert('Session is not ready yet. Please wait for connection.') alert(t('launch.alert_session_not_ready'))
return return
} }
@ -983,11 +1007,11 @@ const uploadFiles = async (files) => {
uploadedAttachments.value.push(result) uploadedAttachments.value.push(result)
} else { } else {
console.error('File upload failed:', result) console.error('File upload failed:', result)
alert(result?.message || 'Failed to upload file') alert(result?.message || t('launch.alert_failed_upload'))
} }
} catch (error) { } catch (error) {
console.error('Failed to upload attachment:', error) console.error('Failed to upload attachment:', error)
alert('File upload failed, please try again.') alert(t('launch.alert_file_upload_failed'))
} }
} }
} finally { } finally {
@ -1081,7 +1105,7 @@ const startRecording = async () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to upload recording:', error) console.error('Failed to upload recording:', error)
alert('Recording upload failed, please try again.') alert(t('launch.alert_recording_upload_failed'))
} finally { } finally {
isUploadingAttachment.value = false isUploadingAttachment.value = false
cleanupRecording() cleanupRecording()
@ -1090,7 +1114,7 @@ const startRecording = async () => {
mediaRecorder.onerror = (event) => { mediaRecorder.onerror = (event) => {
console.error('MediaRecorder error:', event.error) console.error('MediaRecorder error:', event.error)
alert('Recording error occurred') alert(t('launch.alert_recording_error'))
cleanupRecording() cleanupRecording()
} }
@ -1098,7 +1122,7 @@ const startRecording = async () => {
isRecording.value = true isRecording.value = true
} catch (error) { } catch (error) {
console.error('Failed to start recording:', error) console.error('Failed to start recording:', error)
alert('Failed to access microphone. Please check permissions.') alert(t('launch.alert_mic_access_failed'))
cleanupRecording() cleanupRecording()
} }
} }
@ -1312,7 +1336,7 @@ const handleYAMLSelection = async (fileName) => {
if (initialInstructions) { if (initialInstructions) {
addChatNotification(initialInstructions) addChatNotification(initialInstructions)
} else { } else {
addChatNotification("No initial instructions provided") addChatNotification(t('launch.no_initial_instructions'))
} }
// Prefetch sprites for all nodes in the workflow // Prefetch sprites for all nodes in the workflow
@ -1333,7 +1357,7 @@ const handleYAMLSelection = async (fileName) => {
} catch (error) { } catch (error) {
console.error('Failed to load YAML file:', error) console.error('Failed to load YAML file:', error)
workflowYaml.value = {} workflowYaml.value = {}
addChatNotification("Failed to load YAML file") addChatNotification(t('launch.notif_failed_load_yaml'))
nodeSpriteMap.value.clear() nodeSpriteMap.value.clear()
} }
@ -1351,7 +1375,7 @@ const handleButtonClick = () => {
} else if (status.value === 'Completed' || status.value === 'Cancelled') { } else if (status.value === 'Completed' || status.value === 'Cancelled') {
// If Relaunch, restart the same workflow and re-enter Launch state // If Relaunch, restart the same workflow and re-enter Launch state
if (!selectedFile.value) { if (!selectedFile.value) {
alert('Please choose a workflow file') alert(t('launch.alert_choose_workflow'))
return return
} }
@ -1455,7 +1479,7 @@ const establishWebSocketConnection = () => {
if (!sessionId) { if (!sessionId) {
status.value = 'Connection error' status.value = 'Connection error'
alert('Missing session information from server.') alert(t('launch.alert_missing_session'))
resetConnectionState() resetConnectionState()
return return
} }
@ -1478,7 +1502,7 @@ const establishWebSocketConnection = () => {
console.error('WebSocket error:', error) console.error('WebSocket error:', error)
status.value = 'Connection error' status.value = 'Connection error'
alert('WebSocket connection error!') alert(t('launch.alert_ws_error'))
resetConnectionState({ closeSocket: false }) resetConnectionState({ closeSocket: false })
} }
@ -1548,7 +1572,6 @@ onUnmounted(() => {
runningLoadingEntries.value = 0 runningLoadingEntries.value = 0
}) })
const { fromObject, fitView, onPaneReady, onNodesInitialized, setNodes, setEdges, edges } = useVueFlow()
// Fit the view after the pane is ready or nodes are initialized // Fit the view after the pane is ready or nodes are initialized
onPaneReady(() => { onPaneReady(() => {
@ -1752,7 +1775,7 @@ const switchToGraph = async () => {
const launchWorkflow = async () => { const launchWorkflow = async () => {
if (!selectedFile.value) { if (!selectedFile.value) {
alert('Please choose a workflow file') alert(t('launch.alert_choose_workflow'))
return return
} }
@ -1763,7 +1786,7 @@ const launchWorkflow = async () => {
) )
if (!trimmedPrompt && attachmentIds.length === 0) { if (!trimmedPrompt && attachmentIds.length === 0) {
alert('Please enter task prompt or upload files.') alert(t('launch.alert_enter_prompt'))
return return
} }
@ -1772,7 +1795,7 @@ const launchWorkflow = async () => {
!isConnectionReady.value || !isConnectionReady.value ||
!sessionId !sessionId
) { ) {
alert('WebSocket connection is not ready yet.') alert(t('launch.alert_ws_not_ready'))
return return
} }
@ -1817,7 +1840,7 @@ const launchWorkflow = async () => {
const error = await response.json().catch(() => ({})) const error = await response.json().catch(() => ({}))
console.error('Failed to launch workflow:', error) console.error('Failed to launch workflow:', error)
status.value = 'Failed' status.value = 'Failed'
alert(`Failed to launch workflow: ${error.detail || 'Unknown error'}`) alert(`${t('launch.alert_failed_launch')}: ${error.detail || t('launch.unknown_error')}`)
shouldGlow.value = true shouldGlow.value = true
if (isConnectionReady.value) { if (isConnectionReady.value) {
status.value = 'Waiting for launch...' status.value = 'Waiting for launch...'
@ -1826,7 +1849,7 @@ const launchWorkflow = async () => {
} catch (error) { } catch (error) {
console.error('Error calling execute API:', error) console.error('Error calling execute API:', error)
status.value = 'Error' status.value = 'Error'
alert(`Failed to call execute API: ${error.message}`) alert(`${t('launch.alert_failed_execute')}: ${error.message}`)
shouldGlow.value = true shouldGlow.value = true
if (isConnectionReady.value) { if (isConnectionReady.value) {
status.value = 'Waiting for launch...' status.value = 'Waiting for launch...'
@ -1867,7 +1890,7 @@ const downloadArtifact = async (message) => {
document.body.removeChild(link) document.body.removeChild(link)
} catch (error) { } catch (error) {
console.error('Failed to download artifact:', error) console.error('Failed to download artifact:', error)
alert('Failed to download file, please try again.') alert(t('launch.alert_download_failed'))
} finally { } finally {
if (message.loading) { if (message.loading) {
message.loading = false message.loading = false
@ -2176,7 +2199,7 @@ const cancelWorkflow = () => {
if (!isWorkflowRunning.value || !ws) { if (!isWorkflowRunning.value || !ws) {
return return
} }
addChatNotification('Workflow cancelled') addChatNotification(t('launch.workflow_cancelled'))
status.value = 'Cancelled' status.value = 'Cancelled'
isWorkflowRunning.value = false isWorkflowRunning.value = false
sessionIdToDownload = sessionId sessionIdToDownload = sessionId
@ -2208,7 +2231,7 @@ const downloadLogs = async () => {
await fetchLogsZip(sessionIdToDownload) await fetchLogsZip(sessionIdToDownload)
} catch (error) { } catch (error) {
console.error('Download failed:', error) console.error('Download failed:', error)
alert('Download failed, please try again later') alert(t('launch.alert_download_logs_failed'))
} }
} }

View File

@ -1,13 +1,14 @@
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router' 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'
import { useI18n } from 'vue-i18n'
const route = useRoute() const route = useRoute()
const { locale, t } = useI18n()
const renderedContent = ref('') const renderedContent = ref('')
const currentLang = ref('en') // 'zh' for Chinese, 'en' for English
const markdownBody = ref(null) const markdownBody = ref(null)
const md = new MarkdownIt({ const md = new MarkdownIt({
html: true, html: true,
@ -29,7 +30,7 @@ md.use(markdownItAnchor, {
.replace(/\s+/g, '-') .replace(/\s+/g, '-')
}) })
const getTutorialFile = () => (currentLang.value === 'en' ? '/tutorial-en.md' : '/tutorial-zh.md') const getTutorialFile = () => (locale.value === 'en' ? '/tutorial-en.md' : '/tutorial-zh.md')
const scrollToHash = () => { const scrollToHash = () => {
// Wait for DOM to update, then scroll to hash if present // Wait for DOM to update, then scroll to hash if present
@ -58,18 +59,18 @@ const addCopyButtons = () => {
const button = document.createElement('button') const button = document.createElement('button')
button.type = 'button' button.type = 'button'
button.className = 'copy-code-btn' button.className = 'copy-code-btn'
button.textContent = 'Copy' button.textContent = t('tutorial.copy')
button.addEventListener('click', async () => { button.addEventListener('click', async () => {
const code = block.querySelector('code') const code = block.querySelector('code')
const text = code ? code.innerText : block.innerText const text = code ? code.innerText : block.innerText
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
button.textContent = 'Copied' button.textContent = t('tutorial.copied')
setTimeout(() => { setTimeout(() => {
button.textContent = 'Copy' button.textContent = t('tutorial.copy')
}, 1200) }, 1200)
} catch (error) { } catch (error) {
console.error('Failed to copy code: ', error) console.error(t('tutorial.failed_to_copy'), error)
} }
}) })
block.classList.add('has-copy-button') block.classList.add('has-copy-button')
@ -93,20 +94,23 @@ const loadTutorial = async () => {
addCopyButtons() addCopyButtons()
scrollToHash() scrollToHash()
} else { } else {
console.error('Failed to fetch tutorial markdown') console.error(t('tutorial.failed_to_fetch'))
} }
} catch (error) { } catch (error) {
console.error('Failed to load tutorial:', error) console.error(t('tutorial.failed_to_load'), error)
} }
} }
const switchLang = (lang) => { const switchLang = (lang) => {
if (currentLang.value !== lang) { if (locale.value !== lang) {
currentLang.value = lang locale.value = lang
loadTutorial()
} }
} }
watch(locale, () => {
loadTutorial()
})
onMounted(() => { onMounted(() => {
loadTutorial() loadTutorial()
}) })
@ -115,8 +119,8 @@ onMounted(() => {
<template> <template>
<div class="tutorial-view"> <div class="tutorial-view">
<div class="lang-switch"> <div class="lang-switch">
<button :class="{ active: currentLang === 'zh' }" @click="switchLang('zh')">中文</button> <button :class="{ active: locale === 'zh' }" @click="switchLang('zh')">{{ $t('tutorial.zh') }}</button>
<button :class="{ active: currentLang === 'en' }" @click="switchLang('en')">English</button> <button :class="{ active: locale === 'en' }" @click="switchLang('en')">{{ $t('tutorial.en') }}</button>
</div> </div>
<div ref="markdownBody" class="markdown-body" v-html="renderedContent"></div> <div ref="markdownBody" class="markdown-body" v-html="renderedContent"></div>
</div> </div>

View File

@ -11,13 +11,13 @@
<input <input
type="text" type="text"
v-model="searchQuery" v-model="searchQuery"
placeholder="Search" :placeholder="$t('common.search')"
class="search-input" class="search-input"
/> />
</div> </div>
<button class="btn create-btn" @click="openFormGenerator" title="Create New Workflow"> <button class="btn create-btn" @click="openFormGenerator" :title="$t('workflow_list.create')">
<span>Create Workflow</span> <span>{{ $t('workflow_list.create') }}</span>
<span class="plus-icon">+</span> <span class="plus-icon">+</span>
</button> </button>
</div> </div>
@ -25,14 +25,14 @@
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="loading"> <div v-if="loading" class="loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>Loading workflows...</p> <p>{{ $t('workflow_list.loading') }}</p>
</div> </div>
<!-- Error State --> <!-- Error State -->
<div v-else-if="error" class="error-message"> <div v-else-if="error" class="error-message">
<div class="error-icon"></div> <div class="error-icon"></div>
<p>{{ error }}</p> <p>{{ error }}</p>
<button @click="loadWorkflows()" class="retry-button">Retry</button> <button @click="loadWorkflows()" class="retry-button">{{ $t('common.retry') }}</button>
</div> </div>
<!-- File List --> <!-- File List -->
@ -59,7 +59,7 @@
<!-- Empty State --> <!-- Empty State -->
<div v-if="filteredFiles.length === 0" class="empty-state"> <div v-if="filteredFiles.length === 0" class="empty-state">
<p>No workflow files found</p> <p>{{ $t('workflow_list.no_files') }}</p>
</div> </div>
</div> </div>
@ -80,10 +80,12 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { fetchWorkflowsWithDesc } from '../utils/apiFunctions.js' import { fetchWorkflowsWithDesc } from '../utils/apiFunctions.js'
import FormGenerator from '../components/FormGenerator.vue' import FormGenerator from '../components/FormGenerator.vue'
const router = useRouter() const router = useRouter()
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
selected: { selected: {
type: String, type: String,

View File

@ -8,20 +8,20 @@
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
</button> --> </button> -->
<h1 class="workflow-name">{{ workflowName }}</h1> <h1 class="workflow-name">{{ currentWorkflowName }}</h1>
</div> </div>
<div class="content"> <div class="content">
<!-- YAML Editor Tab --> <!-- YAML Editor Tab -->
<div v-if="activeTab === 'yaml'" class="yaml-editor"> <div v-if="activeTab === 'yaml'" class="yaml-editor">
<div v-if="yamlParseError" class="yaml-error"> <div v-if="yamlParseError" class="yaml-error">
YAML Parse Error: {{ yamlParseError }} {{ $t('workflow_view.yaml_parse_error') }} {{ yamlParseError }}
</div> </div>
<textarea <textarea
v-model="yamlTextString" v-model="yamlTextString"
class="yaml-textarea" class="yaml-textarea"
:class="{ 'yaml-error-border': yamlParseError }" :class="{ 'yaml-error-border': yamlParseError }"
placeholder="Loading YAML content..." :placeholder="$t('workflow_view.loading_yaml')"
readonly readonly
></textarea> ></textarea>
</div> </div>
@ -92,7 +92,7 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); openCreateNodeModal(); }" @click.stop="() => { hideContextMenu(); openCreateNodeModal(); }"
> >
Create Node {{ $t('workflow_view.create_node') }}
</div> </div>
</RichTooltip> </RichTooltip>
<div <div
@ -100,7 +100,7 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); openCreateNodeModal(); }" @click.stop="() => { hideContextMenu(); openCreateNodeModal(); }"
> >
Create Node {{ $t('workflow_view.create_node') }}
</div> </div>
</template> </template>
@ -111,7 +111,7 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }" @click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }"
> >
Copy Node {{ $t('workflow_view.copy_node') }}
</div> </div>
</RichTooltip> </RichTooltip>
<div <div
@ -119,14 +119,14 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }" @click.stop="() => { hideContextMenu(); onCopyNodeFromContext(); }"
> >
Copy Node {{ $t('workflow_view.copy_node') }}
</div> </div>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.deleteNode" placement="right"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.deleteNode" placement="right">
<div <div
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }" @click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }"
> >
Delete Node {{ $t('workflow_view.delete_node') }}
</div> </div>
</RichTooltip> </RichTooltip>
<div <div
@ -134,7 +134,7 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }" @click.stop="() => { hideContextMenu(); onDeleteNodeFromContext(); }"
> >
Delete Node {{ $t('workflow_view.delete_node') }}
</div> </div>
</template> </template>
@ -145,7 +145,7 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }" @click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }"
> >
Delete Edge {{ $t('workflow_view.delete_edge') }}
</div> </div>
</RichTooltip> </RichTooltip>
<div <div
@ -153,7 +153,7 @@
class="context-menu-item" class="context-menu-item"
@click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }" @click.stop="() => { hideContextMenu(); onDeleteEdgeFromContext(); }"
> >
Delete Edge {{ $t('workflow_view.delete_edge') }}
</div> </div>
</template> </template>
</div> </div>
@ -163,43 +163,42 @@
<div class="tabs"> <div class="tabs">
<div class="tab-buttons"> <div class="tab-buttons">
<button <button
:class="['tab', { active: activeTab === 'graph' }]" :class="['tab', { active: activeTab === 'graph' }]"
@click="activeTab = 'graph'" @click="activeTab = 'graph'"
> >
Workflow Graph {{ $t('workflow_view.workflow_graph') }}
</button> </button>
<button <button
:class="['tab', { active: activeTab === 'yaml' }]" :class="['tab', { active: activeTab === 'yaml' }]"
@click="activeTab = 'yaml'" @click="activeTab = 'yaml'"
> >
YAML Configuration {{ $t('workflow_view.yaml_configuration') }}
</button> </button> </div>
</div>
<div v-if="activeTab === 'graph'" class="editor-actions"> <div v-if="activeTab === 'graph'" class="editor-actions">
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createNodeButton" placement="bottom"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createNodeButton" placement="bottom">
<button @click="openCreateNodeModal" class="glass-button"> <button @click="openCreateNodeModal" class="glass-button">
<span>Create Node</span> <span>{{ $t('workflow_view.create_node') }}</span>
</button> </button>
</RichTooltip> </RichTooltip>
<button v-else @click="openCreateNodeModal" class="glass-button"> <button v-else @click="openCreateNodeModal" class="glass-button">
<span>Create Node</span> <span>{{ $t('workflow_view.create_node') }}</span>
</button> </button>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.configureGraph" placement="bottom"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.configureGraph" placement="bottom">
<button @click="openConfigureGraphModal" class="glass-button"> <button @click="openConfigureGraphModal" class="glass-button">
<span>Configure Graph</span> <span>{{ $t('workflow_view.configure_graph') }}</span>
</button> </button>
</RichTooltip> </RichTooltip>
<button v-else @click="openConfigureGraphModal" class="glass-button"> <button v-else @click="openConfigureGraphModal" class="glass-button">
<span>Configure Graph</span> <span>{{ $t('workflow_view.configure_graph') }}</span>
</button> </button>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.launch" placement="bottom"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.launch" placement="bottom">
<button @click="goToLaunch" class="launch-button-primary"> <button @click="goToLaunch" class="launch-button-primary">
<span>Launch</span> <span>{{ $t('workflow_view.launch') }}</span>
</button> </button>
</RichTooltip> </RichTooltip>
<button v-else @click="goToLaunch" class="launch-button-primary"> <button v-else @click="goToLaunch" class="launch-button-primary">
<span>Launch</span> <span>{{ $t('workflow_view.launch') }}</span>
</button> </button>
<div <div
@ -218,25 +217,25 @@
<transition name="fade"> <transition name="fade">
<div v-if="showMenu" class="menu-dropdown"> <div v-if="showMenu" class="menu-dropdown">
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.renameWorkflow" placement="left"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.renameWorkflow" placement="left">
<div @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div> <div @click="openRenameWorkflowModal" class="menu-item">{{ $t('workflow_view.rename_workflow') }}</div>
</RichTooltip> </RichTooltip>
<div v-else @click="openRenameWorkflowModal" class="menu-item">Rename Workflow</div> <div v-else @click="openRenameWorkflowModal" class="menu-item">{{ $t('workflow_view.rename_workflow') }}</div>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.copyWorkflow" placement="left"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.copyWorkflow" placement="left">
<div @click="openCopyWorkflowModal" class="menu-item">Copy Workflow</div> <div @click="openCopyWorkflowModal" class="menu-item">{{ $t('workflow_view.copy_workflow') }}</div>
</RichTooltip> </RichTooltip>
<div v-else @click="openCopyWorkflowModal" class="menu-item">Copy Workflow</div> <div v-else @click="openCopyWorkflowModal" class="menu-item">{{ $t('workflow_view.copy_workflow') }}</div>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.manageVariables" placement="left"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.manageVariables" placement="left">
<div @click="openManageVarsModal" class="menu-item">Manage Variables</div> <div @click="openManageVarsModal" class="menu-item">{{ $t('workflow_view.manage_variables') }}</div>
</RichTooltip> </RichTooltip>
<div v-else @click="openManageVarsModal" class="menu-item">Manage Variables</div> <div v-else @click="openManageVarsModal" class="menu-item">{{ $t('workflow_view.manage_variables') }}</div>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.manageMemories" placement="left"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.manageMemories" placement="left">
<div @click="openManageMemoriesModal" class="menu-item">Manage Memories</div> <div @click="openManageMemoriesModal" class="menu-item">{{ $t('workflow_view.manage_memories') }}</div>
</RichTooltip> </RichTooltip>
<div v-else @click="openManageMemoriesModal" class="menu-item">Manage Memories</div> <div v-else @click="openManageMemoriesModal" class="menu-item">{{ $t('workflow_view.manage_memories') }}</div>
<RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createEdge" placement="left"> <RichTooltip v-if="shouldShowTooltip" :content="helpContent.contextMenu.createEdge" placement="left">
<div @click="openCreateEdgeModal" class="menu-item">Create Edge</div> <div @click="openCreateEdgeModal" class="menu-item">{{ $t('workflow_view.create_edge') }}</div>
</RichTooltip> </RichTooltip>
<div v-else @click="openCreateEdgeModal" class="menu-item">Create Edge</div> <div v-else @click="openCreateEdgeModal" class="menu-item">{{ $t('workflow_view.create_edge') }}</div>
</div> </div>
</transition> </transition>
</div> </div>
@ -249,7 +248,7 @@
v-if="showDynamicFormGenerator" v-if="showDynamicFormGenerator"
:breadcrumbs="formGeneratorBreadcrumbs" :breadcrumbs="formGeneratorBreadcrumbs"
:recursive="formGeneratorRecursive" :recursive="formGeneratorRecursive"
:workflow-name="workflowName" :workflow-name="currentWorkflowName"
:initial-yaml="formGeneratorInitialYaml ?? yamlContent" :initial-yaml="formGeneratorInitialYaml ?? yamlContent"
:initial-form-data="formGeneratorInitialFormData" :initial-form-data="formGeneratorInitialFormData"
:mode="formGeneratorMode" :mode="formGeneratorMode"
@ -264,7 +263,7 @@
<div v-if="showRenameModal" class="modal-overlay" @click.self="closeRenameModal"> <div v-if="showRenameModal" class="modal-overlay" @click.self="closeRenameModal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">Rename Workflow</h3> <h3 class="modal-title">{{ $t('workflow_view.rename_workflow') }}</h3>
<button @click="closeRenameModal" class="close-button"> <button @click="closeRenameModal" class="close-button">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@ -273,20 +272,20 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="rename-workflow-name" class="form-label">Workflow Name</label> <label for="rename-workflow-name" class="form-label">{{ $t('workflow_view.workflow_name') }}</label>
<input <input
id="rename-workflow-name" id="rename-workflow-name"
v-model="renameWorkflowName" v-model="renameWorkflowName"
type="text" type="text"
class="form-input" class="form-input"
placeholder="Enter new workflow name" :placeholder="$t('workflow_view.enter_new_name')"
@keyup.enter="handleRenameSubmit" @keyup.enter="handleRenameSubmit"
/> />
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button @click="closeRenameModal" class="cancel-button">Cancel</button> <button @click="closeRenameModal" class="cancel-button">{{ $t('common.cancel') }}</button>
<button @click="handleRenameSubmit" class="submit-button" :disabled="!renameWorkflowName.trim()">Submit</button> <button @click="handleRenameSubmit" class="submit-button" :disabled="!renameWorkflowName.trim()">{{ $t('common.submit') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -317,7 +316,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button @click="closeCopyModal" class="cancel-button">Cancel</button> <button @click="closeCopyModal" class="cancel-button">Cancel</button>
<button @click="handleCopySubmit" class="submit-button" :disabled="!copyWorkflowName.trim()">Submit</button> <button @click="handleCopySubmit" class="submit-button" :disabled="!copyWorkflowName.trim()">{{ $t('common.submit') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -326,6 +325,7 @@
<script setup> <script setup>
import { ref, watch, nextTick, onMounted, onBeforeUnmount, computed } from 'vue' import { ref, watch, nextTick, onMounted, onBeforeUnmount, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
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'
import { Controls } from '@vue-flow/controls' import { Controls } from '@vue-flow/controls'
@ -352,6 +352,7 @@ const props = defineProps({
}) })
const emit = defineEmits(['refresh-workflows']) const emit = defineEmits(['refresh-workflows'])
const router = useRouter() const router = useRouter()
const { t } = useI18n()
const { toObject, fromObject, fitView, getViewport } = useVueFlow() const { toObject, fromObject, fitView, getViewport } = useVueFlow()
const vueflowContainerRef = ref(null) const vueflowContainerRef = ref(null)
@ -365,7 +366,7 @@ const onNodeLeave = (_nodeId) => {
hoveredNodeId.value = null hoveredNodeId.value = null
} }
const workflowName = ref('') const currentWorkflowName = ref('')
const activeTab = ref('graph') const activeTab = ref('graph')
const yamlContent = ref({}) // YAML object const yamlContent = ref({}) // YAML object
const yamlTextString = ref('') // YAML string const yamlTextString = ref('') // YAML string
@ -568,11 +569,11 @@ const dimStartNode = () => {
// Persist an updated YAML snapshot back to the server and refresh local state // Persist an updated YAML snapshot back to the server and refresh local state
const persistYamlSnapshot = async (snapshot) => { const persistYamlSnapshot = async (snapshot) => {
try { try {
if (!workflowName.value) { if (!currentWorkflowName.value) {
return false return false
} }
const yamlString = yaml.dump(snapshot ?? {}) const yamlString = yaml.dump(snapshot ?? {})
const result = await updateYaml(workflowName.value, yamlString) const result = await updateYaml(currentWorkflowName.value, yamlString)
if (!result?.success) { if (!result?.success) {
console.error('Failed to update workflow YAML:', result?.message || result?.detail) console.error('Failed to update workflow YAML:', result?.message || result?.detail)
return false return false
@ -728,8 +729,8 @@ const initializeWorkflow = async (name) => {
if (!name) { if (!name) {
return return
} }
workflowName.value = name currentWorkflowName.value = name
console.log('Workflow initialized: ', workflowName.value) console.log('Workflow initialized: ', currentWorkflowName.value)
await loadYamlFile() await loadYamlFile()
loadAndSyncVueFlowGraph() loadAndSyncVueFlowGraph()
await nextTick() await nextTick()
@ -763,7 +764,7 @@ watch(activeTab, async (newTab) => {
const saveVueFlowGraph = async () => { const saveVueFlowGraph = async () => {
try { try {
const flowObj = toObject() const flowObj = toObject()
const key = workflowName.value const key = currentWorkflowName.value
const result = await postVuegraphs({ const result = await postVuegraphs({
filename: key, filename: key,
content: JSON.stringify(flowObj) content: JSON.stringify(flowObj)
@ -782,7 +783,7 @@ const saveVueFlowGraph = async () => {
const loadAndSyncVueFlowGraph = async () => { const loadAndSyncVueFlowGraph = async () => {
try { try {
const key = workflowName.value const key = currentWorkflowName.value
const result = await fetchVueGraph(key) const result = await fetchVueGraph(key)
if(result?.success === true) { if(result?.success === true) {
@ -824,10 +825,10 @@ const loadAndSyncVueFlowGraph = async () => {
const loadYamlFile = async () => { const loadYamlFile = async () => {
try { try {
if (!workflowName.value) { if (!currentWorkflowName.value) {
return return
} }
const result = await fetchYaml(workflowName.value) const result = await fetchYaml(currentWorkflowName.value)
if (!result?.success) { if (!result?.success) {
console.error('Failed to load YAML file', result?.message || result?.detail) console.error('Failed to load YAML file', result?.message || result?.detail)
@ -1740,12 +1741,12 @@ const openCreateEdgeModal = () => {
} }
const goToLaunch = () => { const goToLaunch = () => {
if (!workflowName.value) { if (!currentWorkflowName.value) {
return return
} }
const fileName = workflowName.value.endsWith('.yaml') const fileName = currentWorkflowName.value.endsWith('.yaml')
? workflowName.value ? currentWorkflowName.value
: `${workflowName.value}.yaml` : `${currentWorkflowName.value}.yaml`
const resolved = router.resolve({ const resolved = router.resolve({
path: '/launch', path: '/launch',
@ -1758,7 +1759,7 @@ const goToLaunch = () => {
// Modal functions for rename and copy workflow // Modal functions for rename and copy workflow
const openRenameWorkflowModal = () => { const openRenameWorkflowModal = () => {
showMenu.value = false showMenu.value = false
renameWorkflowName.value = workflowName.value.replace('.yaml', '') renameWorkflowName.value = currentWorkflowName.value.replace('.yaml', '')
showRenameModal.value = true showRenameModal.value = true
} }
@ -1773,11 +1774,11 @@ const handleRenameSubmit = async () => {
} }
const newName = renameWorkflowName.value.trim() const newName = renameWorkflowName.value.trim()
const result = await postYamlNameChange(workflowName.value, newName) const result = await postYamlNameChange(currentWorkflowName.value, newName)
if (result.success) { if (result.success) {
// Handle VueGraph rename // Handle VueGraph rename
const oldWorkflowKey = workflowName.value.replace('.yaml', '') const oldWorkflowKey = currentWorkflowName.value.replace('.yaml', '')
const newWorkflowKey = newName const newWorkflowKey = newName
// Save VueGraph into new workflow // Save VueGraph into new workflow
@ -1816,7 +1817,7 @@ const handleRenameSubmit = async () => {
const openCopyWorkflowModal = () => { const openCopyWorkflowModal = () => {
showMenu.value = false showMenu.value = false
copyWorkflowName.value = workflowName.value.replace('.yaml', '') + '_copy' copyWorkflowName.value = currentWorkflowName.value.replace('.yaml', '') + '_copy'
showCopyModal.value = true showCopyModal.value = true
} }
@ -1831,11 +1832,11 @@ const handleCopySubmit = async () => {
} }
const newName = copyWorkflowName.value.trim() const newName = copyWorkflowName.value.trim()
const result = await postYamlCopy(workflowName.value, newName) const result = await postYamlCopy(currentWorkflowName.value, newName)
if (result.success) { if (result.success) {
// Handle VueGraph copy // Handle VueGraph copy
const sourceWorkflowKey = workflowName.value.replace('.yaml', '') const sourceWorkflowKey = currentWorkflowName.value.replace('.yaml', '')
const targetWorkflowKey = newName const targetWorkflowKey = newName
try { try {

View File

@ -26,8 +26,8 @@
@refresh-workflows="handleRefreshWorkflows" @refresh-workflows="handleRefreshWorkflows"
/> />
<div v-else class="placeholder"> <div v-else class="placeholder">
<div class="placeholder-title"> Select a workflow</div> <div class="placeholder-title"> {{ $t('workbench.select_workflow') }}</div>
<div class="placeholder-subtitle">Choose a workflow from the list to view or edit.</div> <div class="placeholder-subtitle">{{ $t('workbench.choose_workflow') }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -36,11 +36,13 @@
<script setup> <script setup>
import { onMounted, onBeforeUnmount, ref, watch } from 'vue' import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import WorkflowList from './WorkflowList.vue' import WorkflowList from './WorkflowList.vue'
import WorkflowView from './WorkflowView.vue' import WorkflowView from './WorkflowView.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { t } = useI18n()
const workflowListRef = ref(null) const workflowListRef = ref(null)
const normalizeName = (name) => name?.replace?.('.yaml', '') ?? '' const normalizeName = (name) => name?.replace?.('.yaml', '') ?? ''

View File

@ -5,7 +5,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 ENABLE_HELP_TOOLTIPS: true,
LANGUAGE: 'en'
} }
// Initialize state from localStorage // Initialize state from localStorage

View File

@ -1,11 +1,11 @@
export const helpContent = { export const helpContent = {
// Start Node Help // Start Node Help
startNode: { startNode: {
title: "Start Node", title: "help.startNode.title",
description: "The entry point for your workflow. All nodes connected to the Start node will run in parallel when the workflow launches.", description: "help.startNode.description",
examples: [ examples: [
"Connect multiple nodes to start them simultaneously", "help.startNode.examples.0",
"The first nodes to execute receive your initial input" "help.startNode.examples.1"
], ],
learnMoreUrl: "/tutorial#2-create-nodes" learnMoreUrl: "/tutorial#2-create-nodes"
}, },
@ -13,78 +13,78 @@ export const helpContent = {
// Workflow Node Types // Workflow Node Types
workflowNode: { workflowNode: {
agent: { agent: {
title: "Agent Node", title: "help.workflowNode.agent.title",
description: "An AI agent that can reason, generate content, and use tools. Agents receive messages and produce responses based on their configuration.", description: "help.workflowNode.agent.description",
examples: [ examples: [
"Content generation (writing, coding, analysis)", "help.workflowNode.agent.examples.0",
"Decision making and routing", "help.workflowNode.agent.examples.1",
"Tool usage (search, file operations, API calls)" "help.workflowNode.agent.examples.2"
], ],
learnMoreUrl: "/tutorial#agent-node" learnMoreUrl: "/tutorial#agent-node"
}, },
human: { human: {
title: "Human Node", title: "help.workflowNode.human.title",
description: "Pauses workflow execution and waits for human input. Use this to review content, make decisions, or provide feedback.", description: "help.workflowNode.human.description",
examples: [ examples: [
"Review and approve generated content", "help.workflowNode.human.examples.0",
"Provide additional instructions or corrections", "help.workflowNode.human.examples.1",
"Choose between workflow paths" "help.workflowNode.human.examples.2"
], ],
learnMoreUrl: "/tutorial#human-node" learnMoreUrl: "/tutorial#human-node"
}, },
python: { python: {
title: "Python Node", title: "help.workflowNode.python.title",
description: "Executes Python code on your local environment. The code runs in the workspace directory and can access uploaded files.", description: "help.workflowNode.python.description",
examples: [ examples: [
"Data processing and analysis", "help.workflowNode.python.examples.0",
"Running generated code", "help.workflowNode.python.examples.1",
"File manipulation" "help.workflowNode.python.examples.2"
], ],
learnMoreUrl: "/tutorial#python-node" learnMoreUrl: "/tutorial#python-node"
}, },
passthrough: { passthrough: {
title: "Passthrough Node", title: "help.workflowNode.passthrough.title",
description: "Passes messages to the next node without modification. Useful for workflow organization and filtering outputs in loops.", description: "help.workflowNode.passthrough.description",
examples: [ examples: [
"Preserve initial context in loops", "help.workflowNode.passthrough.examples.0",
"Filter redundant outputs", "help.workflowNode.passthrough.examples.1",
"Organize workflow structure" "help.workflowNode.passthrough.examples.2"
], ],
learnMoreUrl: "/tutorial#passthrough-node" learnMoreUrl: "/tutorial#passthrough-node"
}, },
literal: { literal: {
title: "Literal Node", title: "help.workflowNode.literal.title",
description: "Outputs fixed text, ignoring all input. Use this to inject instructions or context at specific points in the workflow.", description: "help.workflowNode.literal.description",
examples: [ examples: [
"Add fixed instructions before a node", "help.workflowNode.literal.examples.0",
"Inject context or constraints", "help.workflowNode.literal.examples.1",
"Provide test data" "help.workflowNode.literal.examples.2"
], ],
learnMoreUrl: "/tutorial#literal-node" learnMoreUrl: "/tutorial#literal-node"
}, },
loop_counter: { loop_counter: {
title: "Loop Counter Node", title: "help.workflowNode.loop_counter.title",
description: "Limits loop iterations. Only produces output when the maximum count is reached, helping control infinite loops.", description: "help.workflowNode.loop_counter.description",
examples: [ examples: [
"Prevent runaway loops", "help.workflowNode.loop_counter.examples.0",
"Set maximum revision cycles", "help.workflowNode.loop_counter.examples.1",
"Control iterative processes" "help.workflowNode.loop_counter.examples.2"
], ],
learnMoreUrl: "/tutorial#loop-counter-node" learnMoreUrl: "/tutorial#loop-counter-node"
}, },
subgraph: { subgraph: {
title: "Subgraph Node", title: "help.workflowNode.subgraph.title",
description: "Embeds another workflow as a reusable module. Enables modular design and workflow composition.", description: "help.workflowNode.subgraph.description",
examples: [ examples: [
"Reuse common patterns across workflows", "help.workflowNode.subgraph.examples.0",
"Break complex workflows into manageable pieces", "help.workflowNode.subgraph.examples.1",
"Share workflows between teams" "help.workflowNode.subgraph.examples.2"
], ],
learnMoreUrl: "/tutorial#subgraph-node" learnMoreUrl: "/tutorial#subgraph-node"
}, },
unknown: { unknown: {
title: "Workflow Node", title: "help.workflowNode.unknown.title",
description: "A node in your workflow. Click to view and edit its configuration.", description: "help.workflowNode.unknown.description",
learnMoreUrl: "/tutorial#2-create-nodes" learnMoreUrl: "/tutorial#2-create-nodes"
} }
}, },
@ -92,25 +92,25 @@ export const helpContent = {
// Workflow Edge Help // Workflow Edge Help
edge: { edge: {
basic: { basic: {
title: "Connection", title: "help.edge.basic.title",
description: "Connects two nodes to control information flow and execution order. The upstream node's output becomes the downstream node's input.", description: "help.edge.basic.description",
examples: [ examples: [
"Data flows from source to target", "help.edge.basic.examples.0",
"Target executes after source completes" "help.edge.basic.examples.1"
], ],
learnMoreUrl: "/tutorial#what-is-an-edge" learnMoreUrl: "/tutorial#what-is-an-edge"
}, },
trigger: { trigger: {
enabled: { enabled: {
description: "This connection triggers the downstream node to execute.", description: "help.edge.trigger.enabled.description",
}, },
disabled: { disabled: {
description: "This connection passes data but does NOT trigger execution. The downstream node only runs if triggered by another edge.", description: "help.edge.trigger.disabled.description",
} }
}, },
condition: { condition: {
hasCondition: { hasCondition: {
description: "This connection has a condition. It only activates when the condition evaluates to true.", description: "help.edge.condition.hasCondition.description",
learnMoreUrl: "/tutorial#edge-condition" learnMoreUrl: "/tutorial#edge-condition"
} }
} }
@ -119,40 +119,40 @@ export const helpContent = {
// Context Menu Actions // Context Menu Actions
contextMenu: { contextMenu: {
createNode: { createNode: {
description: "Create a new node in your workflow. Choose from Agent, Human, Python, and other node types.", description: "help.contextMenu.createNode.description",
}, },
copyNode: { copyNode: {
description: "Duplicate this node with all its settings. The copy will have a blank ID that you must fill in.", description: "help.contextMenu.copyNode.description",
}, },
deleteNode: { deleteNode: {
description: "Remove this node and all its connections from the workflow.", description: "help.contextMenu.deleteNode.description",
}, },
deleteEdge: { deleteEdge: {
description: "Remove this connection between nodes.", description: "help.contextMenu.deleteEdge.description",
}, },
createNodeButton: { createNodeButton: {
description: "Open the node creation form. You can also right-click the canvas to create a node at a specific position.", description: "help.contextMenu.createNodeButton.description",
}, },
configureGraph: { configureGraph: {
description: "Configure workflow-level settings like name, description, and global variables.", description: "help.contextMenu.configureGraph.description",
}, },
launch: { launch: {
description: "Run your workflow with a task prompt. The workflow will execute and show you the results.", description: "help.contextMenu.launch.description",
}, },
createEdge: { createEdge: {
description: "Create a connection between nodes. You can also drag from a node's handle to create connections visually.", description: "help.contextMenu.createEdge.description",
}, },
manageVariables: { manageVariables: {
description: "Define global variables (like API keys) that all nodes can access using ${VARIABLE_NAME} syntax.", description: "help.contextMenu.manageVariables.description",
}, },
manageMemories: { manageMemories: {
description: "Configure memory modules for long-term information storage and retrieval across workflow runs.", description: "help.contextMenu.manageMemories.description",
}, },
renameWorkflow: { renameWorkflow: {
description: "Change the name of this workflow file.", description: "help.contextMenu.renameWorkflow.description",
}, },
copyWorkflow: { copyWorkflow: {
description: "Create a duplicate of this entire workflow with a new name.", description: "help.contextMenu.copyWorkflow.description",
} }
} }
} }
@ -208,16 +208,17 @@ export function getNodeHelp(nodeType) {
*/ */
export function getEdgeHelp(edgeData) { export function getEdgeHelp(edgeData) {
const base = { ...helpContent.edge.basic } const base = { ...helpContent.edge.basic }
base.descriptions = [base.description] // Change to array to support multiple parts
// Add trigger information // Add trigger information
const trigger = edgeData?.trigger !== undefined ? edgeData.trigger : true const trigger = edgeData?.trigger !== undefined ? edgeData.trigger : true
if (!trigger) { if (!trigger) {
base.description += " " + helpContent.edge.trigger.disabled.description base.descriptions.push(helpContent.edge.trigger.disabled.description)
} }
// Add condition information // Add condition information
if (edgeData?.condition) { if (edgeData?.condition) {
base.description += " " + helpContent.edge.condition.hasCondition.description base.descriptions.push(helpContent.edge.condition.hasCondition.description)
if (!base.learnMoreUrl) { if (!base.learnMoreUrl) {
base.learnMoreUrl = helpContent.edge.condition.hasCondition.learnMoreUrl base.learnMoreUrl = helpContent.edge.condition.hasCondition.learnMoreUrl
} }