Compare commits

...

61 Commits

Author SHA1 Message Date
JzoNg
29cdfcde61 add doc link 2024-08-06 12:22:16 +08:00
JzoNg
8c52beb983 add tip for conversation var 2024-08-06 12:05:40 +08:00
JzoNg
cae482af5d variable assigner only support coversation variable 2024-08-06 10:00:55 +08:00
JzoNg
365c6794c9 default value for conversation value 2024-08-05 16:31:29 +08:00
JzoNg
8165018a3f draft sync 2024-08-02 14:37:13 +08:00
JzoNg
c11e0c72e5 conversation variable modal data fetch 2024-08-02 11:01:56 +08:00
JzoNg
23e2402f8c support json edit in type object 2024-07-31 14:50:17 +08:00
Joel
2fa4b5a036 fix: can choose same variable and can overwrite 2024-07-31 11:22:34 +08:00
Joel
6cde84c182 chore: support selector value badge 2024-07-30 18:19:28 +08:00
Joel
482879e312 feat: use new option card component 2024-07-30 17:40:48 +08:00
Joel
bd44769441 Merge branch 'main' into feat/variable-assigner 2024-07-30 17:37:43 +08:00
JzoNg
6f692028b3 conversation variable modal in preview 2024-07-30 17:09:34 +08:00
JzoNg
df10424950 draft sync 2024-07-30 17:09:34 +08:00
Joel
5edfbac527 chore: handle write mode ui 2024-07-30 16:06:43 +08:00
Joel
f1d7b2a99a chore: not chose self 2024-07-30 15:57:04 +08:00
Joel
350912e35b chore: change copywrite 2024-07-30 15:20:00 +08:00
Joel
f2acce167a fix: not support append var change to append 2024-07-30 15:14:08 +08:00
Joel
60dca1facd chore: var assigner to new 2024-07-30 14:59:00 +08:00
JzoNg
52d637a83b step run 2024-07-29 16:43:51 +08:00
JzoNg
4964ca0736 fix converdation variable use 2024-07-29 15:15:26 +08:00
JzoNg
a43a5893cd use conversation variable 2024-07-29 13:49:16 +08:00
JzoNg
f5ef56ea5e new style of preview 2024-07-29 11:12:00 +08:00
JzoNg
74ce50d534 Merge branch 'main' into tp 2024-07-26 17:40:56 +08:00
JzoNg
3c38e0baa0 Merge branch 'main' into tp 2024-07-26 17:37:50 +08:00
JzoNg
0cc6256790 conversation variable CURD 2024-07-26 17:26:57 +08:00
JzoNg
80e8129b5e array string & array number 2024-07-26 13:43:27 +08:00
JzoNg
3e711369d9 default value 2024-07-25 17:21:11 +08:00
JzoNg
65d54153d0 conversation variable modal 2024-07-25 17:21:11 +08:00
JzoNg
80148b0d6d update style of variable item 2024-07-25 17:21:11 +08:00
JzoNg
398ea5241b conversation variable panel 2024-07-25 17:21:11 +08:00
Joel
c9eac4b0c9 fix: var selector ui 2024-07-25 16:20:50 +08:00
Joel
69a2dc73bf feat: other value type valid 2024-07-25 14:48:36 +08:00
Joel
0dfb61a350 feat: string number valid 2024-07-25 14:10:49 +08:00
Joel
b1d949d2a2 fix: set value not align 2024-07-25 11:43:34 +08:00
Joel
a192e4d5c8 fix: not show number constant value 2024-07-25 11:37:25 +08:00
Joel
2732dada12 fix: var type value picker 2024-07-24 18:26:32 +08:00
Joel
6491a9fe5c fix: constant type popup with 2024-07-24 18:19:23 +08:00
Joel
2709336dbc feat: new var select trigger 2024-07-24 18:05:22 +08:00
Joel
d9182f1425 feat: edit obj support key 2024-07-24 16:41:42 +08:00
Joel
1ceed84302 fix: object value icon to left 2024-07-24 16:15:22 +08:00
Joel
a8bbed2ee9 feat: support icon to left 2024-07-24 16:09:56 +08:00
Joel
003b4b1b08 fix: height flash 2024-07-24 15:59:14 +08:00
Joel
842ab015c1 fix: placeholder out of editor 2024-07-24 15:42:37 +08:00
Joel
2fe51dd65a fix: not select node show 2024-07-24 15:33:05 +08:00
Joel
0c02fdf4d9 feat: json example 2024-07-24 15:20:06 +08:00
Joel
c027b59235 feat: code editor support element 2024-07-24 13:52:37 +08:00
Joel
a4c179e1c0 feat: type value ui to ui design 2024-07-24 11:41:39 +08:00
Joel
189dcc504c fix: constant field no schema error 2024-07-24 11:18:37 +08:00
Joel
f95a6ecb2a Merge branch 'main' into feat/variable-assigner 2024-07-24 10:32:16 +08:00
JzoNg
122a2c5808 assigner node support env 2024-07-22 15:56:15 +08:00
Joel
7141770b64 feat: array type struct 2024-07-22 15:46:19 +08:00
Joel
3eb9eea805 feat: support array file 2024-07-22 15:46:19 +08:00
Joel
21660db136 feat: support object type 2024-07-22 15:46:19 +08:00
Joel
c96fafa020 feat: support number value 2024-07-22 15:46:19 +08:00
Joel
bbf64b9b81 feat: support string value 2024-07-22 15:46:19 +08:00
Joel
287073fe2b feat: assigner node 2024-07-22 15:46:19 +08:00
Joel
035a001223 feat: support show number plus 2024-07-22 15:46:19 +08:00
Joel
dfc993e7f2 feat: write mode card 2024-07-22 15:46:19 +08:00
Joel
e4b738b7f4 feat: support var picker and get var type 2024-07-22 15:46:19 +08:00
Joel
e915ddfe06 feat: add var assigner title and icons 2024-07-22 15:46:19 +08:00
Joel
375a07a954 feat: var struct 2024-07-22 15:46:19 +08:00
82 changed files with 2487 additions and 268 deletions

View File

@@ -4,16 +4,19 @@ import cn from '@/utils/classnames'
type BadgeProps = {
className?: string
text: string
uppercase?: boolean
}
const Badge = ({
className,
text,
uppercase = true,
}: BadgeProps) => {
return (
<div
className={cn(
'inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep system-2xs-medium-uppercase leading-3 text-text-tertiary',
'inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep leading-3 text-text-tertiary',
uppercase ? 'system-2xs-medium-uppercase' : 'system-xs-medium',
className,
)}
>

View File

@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33463 3.33333C2.96643 3.33333 2.66796 3.63181 2.66796 4V10.6667C2.66796 11.0349 2.96643 11.3333 3.33463 11.3333H4.66796C5.03615 11.3333 5.33463 11.6318 5.33463 12V12.8225L7.65833 11.4283C7.76194 11.3662 7.8805 11.3333 8.00132 11.3333H12.0013C12.3695 11.3333 12.668 11.0349 12.668 10.6667C12.668 10.2985 12.9665 10 13.3347 10C13.7028 10 14.0013 10.2985 14.0013 10.6667C14.0013 11.7713 13.1058 12.6667 12.0013 12.6667H8.18598L5.01095 14.5717C4.805 14.6952 4.5485 14.6985 4.33949 14.5801C4.13049 14.4618 4.00129 14.2402 4.00129 14V12.6667H3.33463C2.23006 12.6667 1.33463 11.7713 1.33463 10.6667V4C1.33463 2.89543 2.23006 2 3.33463 2H6.66798C7.03617 2 7.33464 2.29848 7.33464 2.66667C7.33464 3.03486 7.03617 3.33333 6.66798 3.33333H3.33463Z" fill="#354052"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.74113 2.66667C8.74113 2.29848 9.03961 2 9.4078 2H10.331C10.9721 2 11.5177 2.43571 11.6859 3.04075L11.933 3.93004L12.8986 2.77189C13.3045 2.28508 13.9018 2 14.536 2H14.5954C14.9636 2 15.2621 2.29848 15.2621 2.66667C15.2621 3.03486 14.9636 3.33333 14.5954 3.33333H14.536C14.3048 3.33333 14.08 3.43702 13.9227 3.6257L12.367 5.49165L12.8609 7.2689C12.8746 7.31803 12.9105 7.33333 12.9312 7.33333H13.8543C14.2225 7.33333 14.521 7.63181 14.521 8C14.521 8.36819 14.2225 8.66667 13.8543 8.66667H12.9312C12.29 8.66667 11.7444 8.23095 11.5763 7.62591L11.3291 6.73654L10.3634 7.89478C9.95758 8.38159 9.36022 8.66667 8.72604 8.66667H8.66666C8.29847 8.66667 7.99999 8.36819 7.99999 8C7.99999 7.63181 8.29847 7.33333 8.66666 7.33333H8.72604C8.95723 7.33333 9.18204 7.22965 9.33935 7.04096L10.8951 5.17493L10.4012 3.39777C10.3876 3.34863 10.3516 3.33333 10.331 3.33333H9.4078C9.03961 3.33333 8.74113 3.03486 8.74113 2.66667Z" fill="#354052"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,3 @@
<svg width="21" height="8" viewBox="0 0 21 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.646446 3.64645C0.451185 3.84171 0.451185 4.15829 0.646446 4.35355L3.82843 7.53553C4.02369 7.7308 4.34027 7.7308 4.53553 7.53553C4.7308 7.34027 4.7308 7.02369 4.53553 6.82843L1.70711 4L4.53553 1.17157C4.7308 0.976311 4.7308 0.659728 4.53553 0.464466C4.34027 0.269204 4.02369 0.269204 3.82843 0.464466L0.646446 3.64645ZM21 3.5L1 3.5V4.5L21 4.5V3.5Z" fill="#101828" fill-opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@@ -0,0 +1,3 @@
<svg width="26" height="8" viewBox="0 0 26 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.3536 4.35355C25.5488 4.15829 25.5488 3.84171 25.3536 3.64644L22.1716 0.464465C21.9763 0.269202 21.6597 0.269202 21.4645 0.464465C21.2692 0.659727 21.2692 0.976309 21.4645 1.17157L24.2929 4L21.4645 6.82843C21.2692 7.02369 21.2692 7.34027 21.4645 7.53553C21.6597 7.73079 21.9763 7.73079 22.1716 7.53553L25.3536 4.35355ZM3.59058e-08 4.5L25 4.5L25 3.5L-3.59058e-08 3.5L3.59058e-08 4.5Z" fill="#101828" fill-opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="variable assigner">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.71438 4.42875C1.71438 3.22516 2.68954 2.25 3.89313 2.25C4.30734 2.25 4.64313 2.58579 4.64313 3C4.64313 3.41421 4.30734 3.75 3.89313 3.75C3.51796 3.75 3.21438 4.05359 3.21438 4.42875V7.28563C3.21438 7.48454 3.13536 7.6753 2.9947 7.81596L2.81066 8L2.9947 8.18404C3.13536 8.3247 3.21438 8.51546 3.21438 8.71437V11.5713C3.21438 11.9464 3.51796 12.25 3.89313 12.25C4.30734 12.25 4.64313 12.5858 4.64313 13C4.64313 13.4142 4.30734 13.75 3.89313 13.75C2.68954 13.75 1.71438 12.7748 1.71438 11.5713V9.02503L1.21967 8.53033C1.07902 8.38968 1 8.19891 1 8C1 7.80109 1.07902 7.61032 1.21967 7.46967L1.71438 6.97497V4.42875ZM11.3568 3C11.3568 2.58579 11.6925 2.25 12.1068 2.25C13.3103 2.25 14.2855 3.22516 14.2855 4.42875V6.97497L14.7802 7.46967C14.9209 7.61032 14.9999 7.80109 14.9999 8C14.9999 8.19891 14.9209 8.38968 14.7802 8.53033L14.2855 9.02503V11.5713C14.2855 12.7751 13.3095 13.75 12.1068 13.75C11.6925 13.75 11.3568 13.4142 11.3568 13C11.3568 12.5858 11.6925 12.25 12.1068 12.25C12.4815 12.25 12.7855 11.9462 12.7855 11.5713V8.71437C12.7855 8.51546 12.8645 8.3247 13.0052 8.18404L13.1892 8L13.0052 7.81596C12.8645 7.6753 12.7855 7.48454 12.7855 7.28563V4.42875C12.7855 4.05359 12.4819 3.75 12.1068 3.75C11.6925 3.75 11.3568 3.41421 11.3568 3Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 6C5.25 5.58579 5.58579 5.25 6 5.25H10C10.4142 5.25 10.75 5.58579 10.75 6C10.75 6.41421 10.4142 6.75 10 6.75H6C5.58579 6.75 5.25 6.41421 5.25 6Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 10C5.25 9.58579 5.58579 9.25 6 9.25H10C10.4142 9.25 10.75 9.58579 10.75 10C10.75 10.4142 10.4142 10.75 10 10.75H6C5.58579 10.75 5.25 10.4142 5.25 10Z" fill="white"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,57 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon L"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M3.33463 3.33333C2.96643 3.33333 2.66796 3.63181 2.66796 4V10.6667C2.66796 11.0349 2.96643 11.3333 3.33463 11.3333H4.66796C5.03615 11.3333 5.33463 11.6318 5.33463 12V12.8225L7.65833 11.4283C7.76194 11.3662 7.8805 11.3333 8.00132 11.3333H12.0013C12.3695 11.3333 12.668 11.0349 12.668 10.6667C12.668 10.2985 12.9665 10 13.3347 10C13.7028 10 14.0013 10.2985 14.0013 10.6667C14.0013 11.7713 13.1058 12.6667 12.0013 12.6667H8.18598L5.01095 14.5717C4.805 14.6952 4.5485 14.6985 4.33949 14.5801C4.13049 14.4618 4.00129 14.2402 4.00129 14V12.6667H3.33463C2.23006 12.6667 1.33463 11.7713 1.33463 10.6667V4C1.33463 2.89543 2.23006 2 3.33463 2H6.66798C7.03617 2 7.33464 2.29848 7.33464 2.66667C7.33464 3.03486 7.03617 3.33333 6.66798 3.33333H3.33463Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8.74113 2.66667C8.74113 2.29848 9.03961 2 9.4078 2H10.331C10.9721 2 11.5177 2.43571 11.6859 3.04075L11.933 3.93004L12.8986 2.77189C13.3045 2.28508 13.9018 2 14.536 2H14.5954C14.9636 2 15.2621 2.29848 15.2621 2.66667C15.2621 3.03486 14.9636 3.33333 14.5954 3.33333H14.536C14.3048 3.33333 14.08 3.43702 13.9227 3.6257L12.367 5.49165L12.8609 7.2689C12.8746 7.31803 12.9105 7.33333 12.9312 7.33333H13.8543C14.2225 7.33333 14.521 7.63181 14.521 8C14.521 8.36819 14.2225 8.66667 13.8543 8.66667H12.9312C12.29 8.66667 11.7444 8.23095 11.5763 7.62591L11.3291 6.73654L10.3634 7.89478C9.95758 8.38159 9.36022 8.66667 8.72604 8.66667H8.66666C8.29847 8.66667 7.99999 8.36819 7.99999 8C7.99999 7.63181 8.29847 7.33333 8.66666 7.33333H8.72604C8.95723 7.33333 9.18204 7.22965 9.33935 7.04096L10.8951 5.17493L10.4012 3.39777C10.3876 3.34863 10.3516 3.33333 10.331 3.33333H9.4078C9.03961 3.33333 8.74113 3.03486 8.74113 2.66667Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "BubbleX"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './BubbleX.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'BubbleX'
export default Icon

View File

@@ -0,0 +1,27 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "21",
"height": "8",
"viewBox": "0 0 21 8",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M0.646446 3.64645C0.451185 3.84171 0.451185 4.15829 0.646446 4.35355L3.82843 7.53553C4.02369 7.7308 4.34027 7.7308 4.53553 7.53553C4.7308 7.34027 4.7308 7.02369 4.53553 6.82843L1.70711 4L4.53553 1.17157C4.7308 0.976311 4.7308 0.659728 4.53553 0.464466C4.34027 0.269204 4.02369 0.269204 3.82843 0.464466L0.646446 3.64645ZM21 3.5L1 3.5V4.5L21 4.5V3.5Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
"name": "LongArrowLeft"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './LongArrowLeft.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'LongArrowLeft'
export default Icon

View File

@@ -0,0 +1,27 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "26",
"height": "8",
"viewBox": "0 0 26 8",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M25.3536 4.35355C25.5488 4.15829 25.5488 3.84171 25.3536 3.64644L22.1716 0.464465C21.9763 0.269202 21.6597 0.269202 21.4645 0.464465C21.2692 0.659727 21.2692 0.976309 21.4645 1.17157L24.2929 4L21.4645 6.82843C21.2692 7.02369 21.2692 7.34027 21.4645 7.53553C21.6597 7.73079 21.9763 7.73079 22.1716 7.53553L25.3536 4.35355ZM3.59058e-08 4.5L25 4.5L25 3.5L-3.59058e-08 3.5L3.59058e-08 4.5Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
"name": "LongArrowRight"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './LongArrowRight.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'LongArrowRight'
export default Icon

View File

@@ -1,8 +1,11 @@
export { default as Apps02 } from './Apps02'
export { default as BubbleX } from './BubbleX'
export { default as Colors } from './Colors'
export { default as DragHandle } from './DragHandle'
export { default as Env } from './Env'
export { default as Exchange02 } from './Exchange02'
export { default as FileCode } from './FileCode'
export { default as Icon3Dots } from './Icon3Dots'
export { default as LongArrowLeft } from './LongArrowLeft'
export { default as LongArrowRight } from './LongArrowRight'
export { default as Tools } from './Tools'

View File

@@ -0,0 +1,68 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "variable assigner"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M1.71438 4.42875C1.71438 3.22516 2.68954 2.25 3.89313 2.25C4.30734 2.25 4.64313 2.58579 4.64313 3C4.64313 3.41421 4.30734 3.75 3.89313 3.75C3.51796 3.75 3.21438 4.05359 3.21438 4.42875V7.28563C3.21438 7.48454 3.13536 7.6753 2.9947 7.81596L2.81066 8L2.9947 8.18404C3.13536 8.3247 3.21438 8.51546 3.21438 8.71437V11.5713C3.21438 11.9464 3.51796 12.25 3.89313 12.25C4.30734 12.25 4.64313 12.5858 4.64313 13C4.64313 13.4142 4.30734 13.75 3.89313 13.75C2.68954 13.75 1.71438 12.7748 1.71438 11.5713V9.02503L1.21967 8.53033C1.07902 8.38968 1 8.19891 1 8C1 7.80109 1.07902 7.61032 1.21967 7.46967L1.71438 6.97497V4.42875ZM11.3568 3C11.3568 2.58579 11.6925 2.25 12.1068 2.25C13.3103 2.25 14.2855 3.22516 14.2855 4.42875V6.97497L14.7802 7.46967C14.9209 7.61032 14.9999 7.80109 14.9999 8C14.9999 8.19891 14.9209 8.38968 14.7802 8.53033L14.2855 9.02503V11.5713C14.2855 12.7751 13.3095 13.75 12.1068 13.75C11.6925 13.75 11.3568 13.4142 11.3568 13C11.3568 12.5858 11.6925 12.25 12.1068 12.25C12.4815 12.25 12.7855 11.9462 12.7855 11.5713V8.71437C12.7855 8.51546 12.8645 8.3247 13.0052 8.18404L13.1892 8L13.0052 7.81596C12.8645 7.6753 12.7855 7.48454 12.7855 7.28563V4.42875C12.7855 4.05359 12.4819 3.75 12.1068 3.75C11.6925 3.75 11.3568 3.41421 11.3568 3Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M5.25 6C5.25 5.58579 5.58579 5.25 6 5.25H10C10.4142 5.25 10.75 5.58579 10.75 6C10.75 6.41421 10.4142 6.75 10 6.75H6C5.58579 6.75 5.25 6.41421 5.25 6Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M5.25 10C5.25 9.58579 5.58579 9.25 6 9.25H10C10.4142 9.25 10.75 9.58579 10.75 10C10.75 10.4142 10.4142 10.75 10 10.75H6C5.58579 10.75 5.25 10.4142 5.25 10Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Assigner"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Assigner.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Assigner'
export default Icon

View File

@@ -1,4 +1,5 @@
export { default as Answer } from './Answer'
export { default as Assigner } from './Assigner'
export { default as Code } from './Code'
export { default as End } from './End'
export { default as Home } from './Home'

View File

@@ -2,7 +2,7 @@
import type { SVGProps } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import cn from 'classnames'
type InputProps = {
placeholder?: string
@@ -27,10 +27,10 @@ const Input = ({ value, defaultValue, onChange, className = '', wrapperClassName
const { t } = useTranslation()
return (
<div className={`relative inline-flex w-full ${wrapperClassName}`}>
{showPrefix && <span className={s.prefix}>{prefixIcon ?? <GlassIcon className='h-3.5 w-3.5 stroke-current text-gray-700 stroke-2' />}</span>}
{showPrefix && <span className='whitespace-nowrap absolute left-2 self-center'>{prefixIcon ?? <GlassIcon className='h-3.5 w-3.5 stroke-current text-gray-700 stroke-2' />}</span>}
<input
type={type ?? 'text'}
className={`${s.input} ${showPrefix ? '!pl-7' : ''} ${className}`}
className={cn('inline-flex h-7 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-white placeholder:text-gray-400', showPrefix ? '!pl-7' : '', className)}
placeholder={placeholder ?? (showPrefix ? t('common.operation.search') ?? '' : 'please input')}
value={localValue}
onChange={(e) => {

View File

@@ -1,7 +0,0 @@
.input {
@apply inline-flex h-7 w-full py-1 px-2 rounded-lg text-xs leading-normal;
@apply bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-white placeholder:text-gray-400;
}
.prefix {
@apply whitespace-nowrap absolute left-2 self-center
}

View File

@@ -144,7 +144,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative h-full'>
<div className='relative min-h-5'>
<RichTextPlugin
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}

View File

@@ -21,10 +21,10 @@ import {
} from './index'
import cn from '@/utils/classnames'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import TooltipPlus from '@/app/components/base/tooltip-plus'
type WorkflowVariableBlockComponentProps = {
@@ -52,6 +52,7 @@ const WorkflowVariableBlockComponent = ({
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
const node = localWorkflowNodesMap![variables[0]]
const isEnv = isENV(variables)
const isChatVar = isConversationVar(variables)
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -75,11 +76,11 @@ const WorkflowVariableBlockComponent = ({
className={cn(
'mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none',
isSelected ? ' border-[#84ADFF] bg-[#F5F8FF]' : ' border-black/5 bg-white',
!node && !isEnv && '!border-[#F04438] !bg-[#FEF3F2]',
!node && !isEnv && !isChatVar && '!border-[#F04438] !bg-[#FEF3F2]',
)}
ref={ref}
>
{!isEnv && (
{!isEnv && !isChatVar && (
<div className='flex items-center'>
{
node?.type && (
@@ -97,11 +98,12 @@ const WorkflowVariableBlockComponent = ({
</div>
)}
<div className='flex items-center text-primary-600'>
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
{
!node && !isEnv && (
!node && !isEnv && !isChatVar && (
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
)
}
@@ -109,7 +111,7 @@ const WorkflowVariableBlockComponent = ({
</div>
)
if (!node && !isEnv) {
if (!node && !isEnv && !isChatVar) {
return (
<TooltipPlus popupContent={t('workflow.errorMsg.invalidVariable')}>
{Item}

View File

@@ -3,6 +3,7 @@ import { memo } from 'react'
import { BlockEnum } from './types'
import {
Answer,
Assigner,
Code,
End,
Home,
@@ -43,6 +44,7 @@ const getIcon = (type: BlockEnum, className: string) => {
[BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
[BlockEnum.VariableAssigner]: <VariableX className={className} />,
[BlockEnum.VariableAggregator]: <VariableX className={className} />,
[BlockEnum.Assigner]: <Assigner className={className} />,
[BlockEnum.Tool]: <VariableX className={className} />,
[BlockEnum.Iteration]: <Iteration className={className} />,
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
@@ -62,6 +64,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[BlockEnum.TemplateTransform]: 'bg-[#2E90FA]',
[BlockEnum.VariableAssigner]: 'bg-[#2E90FA]',
[BlockEnum.VariableAggregator]: 'bg-[#2E90FA]',
[BlockEnum.Assigner]: 'bg-[#2E90FA]',
[BlockEnum.ParameterExtractor]: 'bg-[#2E90FA]',
}
const BlockIcon: FC<BlockIconProps> = ({

View File

@@ -59,6 +59,11 @@ export const BLOCKS: Block[] = [
type: BlockEnum.VariableAggregator,
title: 'Variable Aggregator',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.Assigner,
title: 'Variable Assigner',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.ParameterExtractor,

View File

@@ -12,6 +12,7 @@ import HttpRequestDefault from './nodes/http/default'
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
import ToolDefault from './nodes/tool/default'
import VariableAssignerDefault from './nodes/variable-assigner/default'
import AssignerDefault from './nodes/assigner/default'
import EndNodeDefault from './nodes/end/default'
import IterationDefault from './nodes/iteration/default'
@@ -133,6 +134,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
checkValid: VariableAssignerDefault.checkValid,
},
[BlockEnum.Assigner]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: AssignerDefault.getAvailablePrevNodes,
getAvailableNextNodes: AssignerDefault.getAvailableNextNodes,
checkValid: AssignerDefault.checkValid,
},
[BlockEnum.VariableAggregator]: {
author: 'Dify',
about: '',
@@ -268,6 +278,12 @@ export const NODES_INITIAL_DATA = {
output_type: '',
...VariableAssignerDefault.defaultValue,
},
[BlockEnum.Assigner]: {
type: BlockEnum.Assigner,
title: '',
desc: '',
...AssignerDefault.defaultValue,
},
[BlockEnum.Tool]: {
type: BlockEnum.Tool,
title: '',

View File

@@ -0,0 +1,24 @@
import { memo } from 'react'
import Button from '@/app/components/base/button'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import { useStore } from '@/app/components/workflow/store'
const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
const handleClick = () => {
setShowChatVariablePanel(true)
setShowEnvPanel(false)
setShowDebugAndPreviewPanel(false)
}
return (
<Button className='p-2' disabled={disabled} onClick={handleClick}>
<BubbleX className='w-4 h-4 text-components-button-secondary-text' />
</Button>
)
}
export default memo(ChatVariableButton)

View File

@@ -1,21 +1,23 @@
import { memo } from 'react'
import Button from '@/app/components/base/button'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { useStore } from '@/app/components/workflow/store'
import cn from '@/utils/classnames'
const EnvButton = () => {
const EnvButton = ({ disabled }: { disabled: boolean }) => {
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
const handleClick = () => {
setShowEnvPanel(true)
setShowChatVariablePanel(false)
setShowDebugAndPreviewPanel(false)
}
return (
<div className={cn('relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs cursor-pointer hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover')} onClick={handleClick}>
<Button className='p-2' disabled={disabled} onClick={handleClick}>
<Env className='w-4 h-4 text-components-button-secondary-text' />
</div>
</Button>
)
}

View File

@@ -19,6 +19,7 @@ import {
import type { StartNodeType } from '../nodes/start/types'
import {
useChecklistBeforePublish,
useIsChatMode,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowMode,
@@ -31,6 +32,7 @@ import EditingTitle from './editing-title'
import RunningTitle from './running-title'
import RestoringTitle from './restoring-title'
import ViewHistory from './view-history'
import ChatVariableButton from './chat-variable-button'
import EnvButton from './env-button'
import Button from '@/app/components/base/button'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -44,7 +46,8 @@ const Header: FC = () => {
const appDetail = useAppStore(s => s.appDetail)
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
const appID = appDetail?.id
const { getNodesReadOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
@@ -165,7 +168,8 @@ const Header: FC = () => {
{
normal && (
<div className='flex items-center gap-2'>
<EnvButton />
{isChatMode && <ChatVariableButton disabled={nodesReadOnly} />}
<EnvButton disabled={nodesReadOnly} />
<div className='w-[1px] h-3.5 bg-gray-200'></div>
<RunAndHistory />
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
@@ -176,7 +180,7 @@ const Header: FC = () => {
{...{
publishedAt,
draftUpdatedAt,
disabled: Boolean(getNodesReadOnly()),
disabled: nodesReadOnly,
toolPublished,
inputs: variables,
onRefreshData: handleToolConfigureUpdate,

View File

@@ -31,6 +31,7 @@ export const useNodesSyncDraft = () => {
const [x, y, zoom] = transform
const {
appId,
conversationVariables,
environmentVariables,
syncWorkflowDraftHash,
} = workflowStore.getState()
@@ -82,6 +83,7 @@ export const useNodesSyncDraft = () => {
file_upload: features.file,
},
environment_variables: environmentVariables,
conversation_variables: conversationVariables,
hash: syncWorkflowDraftHash,
},
}

View File

@@ -68,6 +68,7 @@ export const useWorkflowUpdate = () => {
setIsSyncingWorkflowDraft,
setEnvironmentVariables,
setEnvSecrets,
setConversationVariables,
} = workflowStore.getState()
setIsSyncingWorkflowDraft(true)
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
@@ -78,6 +79,8 @@ export const useWorkflowUpdate = () => {
return acc
}, {} as Record<string, string>))
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
// #TODO chatVar sync#
setConversationVariables(response.conversation_variables || [])
}).finally(() => setIsSyncingWorkflowDraft(false))
}, [handleUpdateWorkflowCanvas, workflowStore])

View File

@@ -67,9 +67,11 @@ export const useWorkflowStartRun = () => {
setShowDebugAndPreviewPanel,
setHistoryWorkflowData,
setShowEnvPanel,
setShowChatVariablePanel,
} = workflowStore.getState()
setShowEnvPanel(false)
setShowChatVariablePanel(false)
if (showDebugAndPreviewPanel)
handleCancelDebugAndPreviewPanel()

View File

@@ -12,6 +12,7 @@ import type {
export const useWorkflowVariables = () => {
const { t } = useTranslation()
const environmentVariables = useStore(s => s.environmentVariables)
const conversationVariables = useStore(s => s.conversationVariables)
const getNodeAvailableVars = useCallback(({
parentNode,
@@ -19,12 +20,14 @@ export const useWorkflowVariables = () => {
isChatMode,
filterVar,
hideEnv,
hideChatVar,
}: {
parentNode?: Node | null
beforeNodes: Node[]
isChatMode: boolean
filterVar: (payload: Var, selector: ValueSelector) => boolean
hideEnv?: boolean
hideChatVar?: boolean
}): NodeOutPutVar[] => {
return toNodeAvailableVars({
parentNode,
@@ -32,9 +35,10 @@ export const useWorkflowVariables = () => {
beforeNodes,
isChatMode,
environmentVariables: hideEnv ? [] : environmentVariables,
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
filterVar,
})
}, [environmentVariables, t])
}, [conversationVariables, environmentVariables, t])
const getCurrentVariableType = useCallback(({
parentNode,
@@ -59,8 +63,9 @@ export const useWorkflowVariables = () => {
isChatMode,
isConstant,
environmentVariables,
conversationVariables,
})
}, [environmentVariables])
}, [conversationVariables, environmentVariables])
return {
getNodeAvailableVars,

View File

@@ -478,6 +478,8 @@ export const useWorkflowInit = () => {
return acc
}, {} as Record<string, string>),
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
// #TODO chatVar sync#
conversationVariables: res.conversation_variables || [],
})
setSyncWorkflowDraftHash(res.hash)
setIsLoading(false)
@@ -498,6 +500,7 @@ export const useWorkflowInit = () => {
retriever_resource: { enabled: true },
},
environment_variables: [],
conversation_variables: [],
},
}).then((res) => {
workflowStore.getState().setDraftUpdatedAt(res.updated_at)

View File

@@ -64,6 +64,7 @@ const AddVariablePopupWithPosition = ({
} as any,
],
hideEnv: true,
hideChatVar: true,
isChatMode,
filterVar: filterVar(outputType as VarType),
})

View File

@@ -18,6 +18,8 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames'
type Props = {
payload: InputVar
@@ -56,22 +58,24 @@ const FormItem: FC<Props> = ({
}, [value, onChange])
const nodeKey = (() => {
if (typeof payload.label === 'object') {
const { nodeType, nodeName, variable } = payload.label
const { nodeType, nodeName, variable, isChatVar } = payload.label
return (
<div className='h-full flex items-center'>
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon type={nodeType || BlockEnum.Start} />
{!isChatVar && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon type={nodeType || BlockEnum.Start} />
</div>
<div className='mx-0.5 text-xs font-medium text-gray-700 max-w-[150px] truncate' title={nodeName}>
{nodeName}
</div>
<Line3 className='mr-0.5'></Line3>
</div>
<div className='mx-0.5 text-xs font-medium text-gray-700 max-w-[150px] truncate' title={nodeName}>
{nodeName}
</div>
<Line3 className='mr-0.5'></Line3>
</div>
)}
<div className='flex items-center text-primary-600'>
<Variable02 className='w-3.5 h-3.5' />
<div className='ml-0.5 text-xs font-medium max-w-[150px] truncate' title={variable} >
{!isChatVar && <Variable02 className='w-3.5 h-3.5' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('ml-0.5 text-xs font-medium max-w-[150px] truncate', isChatVar && 'text-text-secondary')} title={variable} >
{variable}
</div>
</div>
@@ -86,7 +90,12 @@ const FormItem: FC<Props> = ({
const isIterator = type === InputVarType.iterator
return (
<div className={`${className}`}>
{!isArrayLikeType && <div className='h-8 leading-8 text-[13px] font-medium text-gray-700 truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>}
{!isArrayLikeType && (
<div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
{!payload.required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
</div>
)}
<div className='grow'>
{
type === InputVarType.textInput && (

View File

@@ -15,7 +15,7 @@ const CODE_EDITOR_LINE_HEIGHT = 18
export type Props = {
value?: string | object
placeholder?: string
placeholder?: JSX.Element | string
onChange?: (value: string) => void
title?: JSX.Element
language: CodeLanguage
@@ -167,7 +167,7 @@ const CodeEditor: FC<Props> = ({
}}
onMount={handleEditorDidMount}
/>
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
{!outPutValue && !isFocus && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
</>
)

View File

@@ -26,6 +26,7 @@ type Props = {
justVar?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
insertVarTipToLeft?: boolean
}
const Editor: FC<Props> = ({
@@ -40,6 +41,7 @@ const Editor: FC<Props> = ({
readOnly,
nodesOutputVars,
availableNodes = [],
insertVarTipToLeft,
}) => {
const { t } = useTranslation()
@@ -106,12 +108,12 @@ const Editor: FC<Props> = ({
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
{isFocus && (
<div className='absolute z-10 top-[-9px] right-1'>
<div className={cn('absolute z-10', insertVarTipToLeft ? 'top-1.5 left-[-12px]' : ' top-[-9px] right-1')}>
<TooltipPlus
popupContent={`${t('workflow.common.insertVarTip')}`}
>
<div className='p-0.5 rounded-[5px] shadow-lg cursor-pointer bg-white hover:bg-gray-100 border-[0.5px] border-black/5'>
<Variable02 className='w-3.5 h-3.5 text-gray-500' />
<Variable02 className='w-3.5 h-3.5 text-components-button-secondary-accent-text' />
</div>
</TooltipPlus>
</div>

View File

@@ -5,10 +5,10 @@ import cn from 'classnames'
import { useWorkflow } from '../../../hooks'
import { BlockEnum } from '../../../types'
import { VarBlockIcon } from '../../../block-icon'
import { getNodeInfoById, isENV, isSystemVar } from './variable/utils'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './variable/utils'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
type Props = {
nodeId: string
value: string
@@ -42,13 +42,14 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
const value = vars[index].split('.')
const isSystem = isSystemVar(value)
const isEnv = isENV(value)
const isChatVar = isConversationVar(value)
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`
return (<span key={index}>
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
<div className=' inline-flex h-[16px] items-center px-1.5 rounded-[5px] bg-white'>
{!isEnv && (
{!isEnv && !isChatVar && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
@@ -61,9 +62,10 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
</div>
)}
<div className='flex items-center text-primary-600'>
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
</div>
</div>
</span>)

View File

@@ -10,6 +10,7 @@ type Item = {
label: string
}
type Props = {
className?: string
trigger?: JSX.Element
DropDownIcon?: any
noLeft?: boolean
@@ -27,6 +28,7 @@ type Props = {
}
const TypeSelector: FC<Props> = ({
className,
trigger,
DropDownIcon = ChevronSelectorVertical,
noLeft,
@@ -50,11 +52,12 @@ const TypeSelector: FC<Props> = ({
setHide()
}, ref)
return (
<div className={cn(!trigger && !noLeft && 'left-[-8px]', 'relative')} ref={ref}>
<div className={cn(!trigger && !noLeft && 'left-[-8px]', 'relative select-none', className)} ref={ref}>
{trigger
? (
<div
onClick={toggleShow}
className={cn(!readonly && 'cursor-pointer')}
>
{trigger}
</div>
@@ -63,13 +66,13 @@ const TypeSelector: FC<Props> = ({
<div
onClick={toggleShow}
className={cn(showOption && 'bg-black/5', 'flex items-center h-5 pl-1 pr-0.5 rounded-md text-xs font-semibold text-gray-700 cursor-pointer hover:bg-black/5')}>
<div className={cn(triggerClassName, 'text-xs font-semibold', uppercase && 'uppercase', noValue && 'text-gray-400')}>{!noValue ? item?.label : placeholder}</div>
<div className={cn('text-sm font-semibold', uppercase && 'uppercase', noValue && 'text-gray-400', triggerClassName)}>{!noValue ? item?.label : placeholder}</div>
{!readonly && <DropDownIcon className='w-3 h-3 ' />}
</div>
)}
{(showOption && !readonly) && (
<div className={cn(popupClassName, 'absolute z-10 top-[24px] w-[120px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white')}>
<div className={cn('absolute z-10 top-[24px] w-[120px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white select-none', popupClassName)}>
{list.map(item => (
<div
key={item.value}

View File

@@ -10,8 +10,8 @@ import type {
import { BlockEnum } from '@/app/components/workflow/types'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import cn from '@/utils/classnames'
type VariableTagProps = {
@@ -30,12 +30,13 @@ const VariableTag = ({
return nodes.find(node => node.id === valueSelector[0])
}, [nodes, valueSelector])
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
return (
<div className='inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs'>
{!isEnv && (
{!isEnv && !isChatVar && (
<>
{node && (
<VarBlockIcon
@@ -54,8 +55,9 @@ const VariableTag = ({
</>
)}
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div
className={cn('truncate text-text-accent font-medium', isEnv && 'text-text-secondary')}
className={cn('truncate text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
title={variableName}
>
{variableName}

View File

@@ -9,14 +9,14 @@ import type { Var } from '@/app/components/workflow/types'
import { SimpleSelect } from '@/app/components/base/select'
type Props = {
schema: CredentialFormSchema
schema: Partial<CredentialFormSchema>
readonly: boolean
value: string
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
}
const ConstantField: FC<Props> = ({
schema,
schema = {} as CredentialFormSchema,
readonly,
value,
onChange,
@@ -47,7 +47,7 @@ const ConstantField: FC<Props> = ({
{schema.type === FormTypeEnum.textNumber && (
<input
type='number'
className='w-full h-8 leading-8 pl-0.5 bg-transparent text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
className='w-full h-8 leading-8 p-2 rounded-lg bg-gray-100 text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
value={value}
onChange={handleStaticChange}
readOnly={readonly}

View File

@@ -15,7 +15,7 @@ import type { ParameterExtractorNodeType } from '../../../parameter-extractor/ty
import type { IterationNodeType } from '../../../iteration/types'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
import {
HTTP_REQUEST_OUTPUT_STRUCT,
@@ -38,6 +38,10 @@ export const isENV = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'env'
}
export const isConversationVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'conversation'
}
const inputVarTypeToVarType = (type: InputVarType): VarType => {
if (type === InputVarType.number)
return VarType.number
@@ -244,13 +248,32 @@ const formatItem = (
}) as Var[]
break
}
case 'conversation': {
res.vars = data.chatVarList.map((chatVar: ConversationVariable) => {
return {
variable: `conversation.${chatVar.name}`,
type: chatVar.value_type,
des: chatVar.description,
}
}) as Var[]
break
}
}
const selector = [id]
res.vars = res.vars.filter((v) => {
const { children } = v
if (!children)
return filterVar(v, selector)
if (!children) {
return filterVar(v, (() => {
const variableArr = v.variable.split('.')
const [first, ..._other] = variableArr
if (first === 'sys' || first === 'env' || first === 'conversation')
return variableArr
return [...selector, ...variableArr]
})())
}
const obj = findExceptVarInObject(v, filterVar, selector)
return obj?.children && obj?.children.length > 0
@@ -269,6 +292,7 @@ export const toNodeOutputVars = (
isChatMode: boolean,
filterVar = (_payload: Var, _selector: ValueSelector) => true,
environmentVariables: EnvironmentVariable[] = [],
conversationVariables: ConversationVariable[] = [],
): NodeOutPutVar[] => {
// ENV_NODE data format
const ENV_NODE = {
@@ -279,9 +303,19 @@ export const toNodeOutputVars = (
envList: environmentVariables,
},
}
// CHAT_VAR_NODE data format
const CHAT_VAR_NODE = {
id: 'conversation',
data: {
title: 'CONVERSATION',
type: 'conversation',
chatVarList: conversationVariables,
},
}
const res = [
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length) > 0 ? [CHAT_VAR_NODE] : []),
].map((node) => {
return {
...formatItem(node, isChatMode, filterVar),
@@ -346,6 +380,7 @@ export const getVarType = ({
isChatMode,
isConstant,
environmentVariables = [],
conversationVariables = [],
}:
{
valueSelector: ValueSelector
@@ -355,6 +390,7 @@ export const getVarType = ({
isChatMode: boolean
isConstant?: boolean
environmentVariables?: EnvironmentVariable[]
conversationVariables?: ConversationVariable[]
}): VarType => {
if (isConstant)
return VarType.string
@@ -364,6 +400,7 @@ export const getVarType = ({
isChatMode,
undefined,
environmentVariables,
conversationVariables,
)
const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
@@ -386,6 +423,7 @@ export const getVarType = ({
}
const isSystem = isSystemVar(valueSelector)
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const startNode = availableNodes.find((node: any) => {
return node.data.type === BlockEnum.Start
})
@@ -398,7 +436,7 @@ export const getVarType = ({
let type: VarType = VarType.string
let curr: any = targetVar.vars
if (isSystem || isEnv) {
if (isSystem || isEnv || isChatVar) {
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
}
else {
@@ -424,6 +462,7 @@ export const toNodeAvailableVars = ({
beforeNodes,
isChatMode,
environmentVariables,
conversationVariables,
filterVar,
}: {
parentNode?: Node | null
@@ -433,6 +472,8 @@ export const toNodeAvailableVars = ({
isChatMode: boolean
// env
environmentVariables?: EnvironmentVariable[]
// chat var
conversationVariables?: ConversationVariable[]
filterVar: (payload: Var, selector: ValueSelector) => boolean
}): NodeOutPutVar[] => {
const beforeNodesOutputVars = toNodeOutputVars(
@@ -440,6 +481,7 @@ export const toNodeAvailableVars = ({
isChatMode,
filterVar,
environmentVariables,
conversationVariables,
)
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
if (isInIteration) {
@@ -451,6 +493,7 @@ export const toNodeAvailableVars = ({
availableNodes: beforeNodes,
isChatMode,
environmentVariables,
conversationVariables,
})
const iterationVar = {
nodeId: iterationNode?.id,

View File

@@ -9,7 +9,7 @@ import {
import produce from 'immer'
import { useStoreApi } from 'reactflow'
import VarReferencePopup from './var-reference-popup'
import { getNodeInfoById, isENV, isSystemVar } from './utils'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './utils'
import ConstantField from './constant-field'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
@@ -17,7 +17,7 @@ import type { CredentialFormSchema } from '@/app/components/header/account-setti
import { BlockEnum } from '@/app/components/workflow/types'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import {
PortalToFollowElem,
@@ -32,6 +32,7 @@ import {
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge'
const TRIGGER_DEFAULT_WIDTH = 227
type Props = {
@@ -49,7 +50,8 @@ type Props = {
availableNodes?: Node[]
availableVars?: NodeOutPutVar[]
isAddBtnTrigger?: boolean
schema?: CredentialFormSchema
schema?: Partial<CredentialFormSchema>
valueTypePlaceHolder?: string
}
const VarReferencePicker: FC<Props> = ({
@@ -57,7 +59,7 @@ const VarReferencePicker: FC<Props> = ({
readonly,
className,
isShowNodeName,
value,
value = [],
onOpen = () => { },
onChange,
isSupportConstantValue,
@@ -68,6 +70,7 @@ const VarReferencePicker: FC<Props> = ({
availableVars,
isAddBtnTrigger,
schema,
valueTypePlaceHolder,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@@ -99,7 +102,6 @@ const VarReferencePicker: FC<Props> = ({
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
const outputVars = useMemo(() => {
if (availableVars)
return availableVars
@@ -215,6 +217,7 @@ const VarReferencePicker: FC<Props> = ({
})
const isEnv = isENV(value as ValueSelector)
const isChatVar = isConversationVar(value as ValueSelector)
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
const availableWidth = triggerWidth - 56
@@ -227,6 +230,8 @@ const VarReferencePicker: FC<Props> = ({
return [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth]
})()
const WrapElem = isSupportConstantValue ? 'div' : PortalToFollowElemTrigger
const VarPickerWrap = !isSupportConstantValue ? 'div' : PortalToFollowElemTrigger
return (
<div className={cn(className, !readonly && 'cursor-pointer')}>
<PortalToFollowElem
@@ -234,7 +239,7 @@ const VarReferencePicker: FC<Props> = ({
onOpenChange={setOpen}
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
>
<PortalToFollowElemTrigger onClick={() => {
<WrapElem onClick={() => {
if (readonly)
return
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
@@ -245,23 +250,28 @@ const VarReferencePicker: FC<Props> = ({
<AddButton onClick={() => { }}></AddButton>
</div>
)
: (<div ref={triggerRef} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8 p-1 rounded-lg bg-gray-100 border')}>
: (<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8', !isSupportConstantValue && 'p-1 rounded-lg bg-gray-100 border')}>
{isSupportConstantValue
? <div onClick={(e) => {
e.stopPropagation()
setOpen(false)
setControlFocus(Date.now())
}} className='mr-1 flex items-center space-x-1'>
}} className='h-full mr-1 flex items-center space-x-1'>
<TypeSelector
noLeft
triggerClassName='!text-xs'
trigger={
<div className='flex items-center h-8 px-2 radius-md bg-components-input-bg-normal'>
<div className='mr-1 system-sm-regular text-components-input-text-filled'>{varKindTypes.find(item => item.value === varKindType)?.label}</div>
<RiArrowDownSLine className='w-4 h-4 text-text-quaternary' />
</div>
}
popupClassName='top-8'
readonly={readonly}
DropDownIcon={RiArrowDownSLine}
value={varKindType}
options={varKindTypes}
onChange={handleVarKindTypeChange}
showChecked
/>
<div className='h-4 w-px bg-black/5'></div>
</div>
: (!hasValue && <div className='ml-1.5 mr-1'>
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
@@ -276,38 +286,51 @@ const VarReferencePicker: FC<Props> = ({
/>
)
: (
<div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}>
{hasValue
? (
<>
{isShowNodeName && !isEnv && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-900'
type={outputVarNode?.type || BlockEnum.Start}
/>
<VarPickerWrap
onClick={() => {
if (readonly)
return
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
}}
className='grow h-full'
>
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center pl-1 py-1 rounded-lg bg-gray-100')}>
<div className={cn('h-full items-center px-1.5 rounded-[5px]', hasValue ? 'bg-white inline-flex' : 'flex')}>
{hasValue
? (
<>
{isShowNodeName && !isEnv && !isChatVar && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-900'
type={outputVarNode?.type || BlockEnum.Start}
/>
</div>
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
maxWidth: maxNodeNameWidth,
}}>{outputVarNode?.title}</div>
<Line3 className='mr-0.5'></Line3>
</div>
)}
<div className='flex items-center text-primary-600'>
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!text-text-secondary')} title={varName} style={{
maxWidth: maxVarNameWidth,
}}>{varName}</div>
</div>
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
maxWidth: maxNodeNameWidth,
}}>{outputVarNode?.title}</div>
<Line3 className='mr-0.5'></Line3>
</div>
)}
<div className='flex items-center text-primary-600'>
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-gray-900')} title={varName} style={{
maxWidth: maxVarNameWidth,
}}>{varName}</div>
</div>
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
maxWidth: maxTypeWidth,
}}>{type}</div>
</>
)
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
</div>
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
maxWidth: maxTypeWidth,
}}>{type}</div>
</>
)
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
</div>
</div>
</VarPickerWrap>
)}
{(hasValue && !readonly) && (<div
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
@@ -315,8 +338,15 @@ const VarReferencePicker: FC<Props> = ({
>
<RiCloseLine className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
</div>)}
{!hasValue && valueTypePlaceHolder && (
<Badge
className=' absolute right-1 top-[50%] translate-y-[-50%] capitalize'
text={valueTypePlaceHolder}
uppercase={false}
/>
)}
</div>)}
</PortalToFollowElemTrigger>
</WrapElem>
<PortalToFollowElemContent style={{
zIndex: 100,
}}>

View File

@@ -16,7 +16,7 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { checkKeys } from '@/utils/var'
type ObjectChildrenProps = {
@@ -51,6 +51,7 @@ const Item: FC<ItemProps> = ({
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
const isSys = itemData.variable.startsWith('sys.')
const isEnv = itemData.variable.startsWith('env.')
const isChatVar = itemData.variable.startsWith('conversation.')
const itemRef = useRef(null)
const [isItemHovering, setIsItemHovering] = useState(false)
const _ = useHover(itemRef, {
@@ -79,7 +80,7 @@ const Item: FC<ItemProps> = ({
}, [isHovering])
const handleChosen = (e: React.MouseEvent) => {
e.stopPropagation()
if (isSys || isEnv) { // system variable or environment variable
if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
onChange([...objPath, ...itemData.variable.split('.')], itemData)
}
else {
@@ -100,13 +101,21 @@ const Item: FC<ItemProps> = ({
isHovering && (isObj ? 'bg-primary-50' : 'bg-gray-50'),
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
}
// style={{ width: itemWidth || 252 }}
onClick={handleChosen}
>
<div className='flex items-center w-0 grow'>
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{!isEnv ? itemData.variable : itemData.variable.replace('env.', '')}</div>
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
{!isEnv && !isChatVar && (
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable}</div>
)}
{isEnv && (
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable.replace('env.', '')}</div>
)}
{isChatVar && (
<div title={itemData.des} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable.replace('conversation.', '')}</div>
)}
</div>
<div className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
{isObj && (
@@ -211,7 +220,7 @@ const VarReferenceVars: FC<Props> = ({
const [searchText, setSearchText] = useState('')
const filteredVars = vars.filter((v) => {
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.'))
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
return children.length > 0
}).filter((node) => {
if (!searchText)
@@ -222,7 +231,7 @@ const VarReferenceVars: FC<Props> = ({
})
return children.length > 0
}).map((node) => {
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.'))
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
if (searchText) {
const searchTextLower = searchText.toLowerCase()
if (!node.title.toLowerCase().includes(searchTextLower))

View File

@@ -7,12 +7,12 @@ import {
useNodeDataUpdate,
useWorkflow,
} from '@/app/components/workflow/hooks'
import { getNodeInfoById, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { getIterationSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
import Toast from '@/app/components/base/toast'
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
@@ -95,12 +95,13 @@ const useOneStepRun = <T>({
}: Params<T>) => {
const { t } = useTranslation()
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
const conversationVariables = useStore(s => s.conversationVariables)
const isChatMode = useIsChatMode()
const isIteration = data.type === BlockEnum.Iteration
const availableNodes = getBeforeNodesInSameBranch(id)
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
const getVar = (valueSelector: ValueSelector): Var | undefined => {
let res: Var | undefined
const isSystem = valueSelector[0] === 'sys'
@@ -116,7 +117,8 @@ const useOneStepRun = <T>({
valueSelector.slice(1).forEach((key, i) => {
const isLast = i === valueSelector.length - 2
curr = curr?.find((v: any) => v.variable === key)
// conversation variable is start with 'conversation.'
curr = curr?.find((v: any) => v.variable.replace('conversation.', '') === key)
if (isLast) {
res = curr
}
@@ -369,6 +371,7 @@ const useOneStepRun = <T>({
nodeType: varInfo?.type,
nodeName: varInfo?.title || availableNodesIncludeParent[0]?.data.title, // default start node title
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
isChatVar: isConversationVar(item),
},
variable: `#${item.join('.')}#`,
value_selector: item,

View File

@@ -0,0 +1,46 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<AssignerNodeType> = {
defaultValue: {
assigned_variable_selector: [],
write_mode: WriteMode.Overwrite,
input_variable_selector: [],
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: AssignerNodeType, t: any) {
let errorMessages = ''
const {
assigned_variable_selector: assignedVarSelector,
write_mode: writeMode,
input_variable_selector: toAssignerVarSelector,
} = payload
if (!errorMessages && !assignedVarSelector?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.assignedVariable') })
if (!errorMessages && writeMode !== WriteMode.Clear) {
if (!toAssignerVarSelector?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.variable') })
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,47 @@
import type { FC } from 'react'
import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type AssignerNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.assigner'
const NodeComponent: FC<NodeProps<AssignerNodeType>> = ({
data,
}) => {
const { t } = useTranslation()
const nodes: Node[] = useNodes()
const { assigned_variable_selector: variable, write_mode: writeMode } = data
if (!variable || variable.length === 0)
return null
const isSystem = isSystemVar(variable)
const isEnv = isENV(variable)
const isChatVar = isConversationVar(variable)
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
return (
<div className='relative px-3'>
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.assignedVariable`)}</div>
<NodeVariableItem
node={node as Node}
isEnv={isEnv}
isChatVar={isChatVar}
varName={varName}
className='bg-workflow-block-parma-bg'
/>
<div className='my-2 flex justify-between items-center h-[22px] px-[5px] bg-workflow-block-parma-bg radius-sm'>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.writeMode`)}</div>
<div className='system-xs-medium text-text-secondary'>{t(`${i18nPrefix}.${writeMode}`)}</div>
</div>
</div>
)
}
export default React.memo(NodeComponent)

View File

@@ -0,0 +1,85 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import OptionCard from '../_base/components/option-card'
import useConfig from './use-config'
import { WriteMode } from './types'
import type { AssignerNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { type NodePanelProps } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.assigner'
const Panel: FC<NodePanelProps<AssignerNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleAssignedVarChanges,
isSupportAppend,
writeModeTypes,
handleWriteModeChange,
filterAssignedVar,
filterToAssignedVar,
handleToAssignedVarChange,
toAssignedVarType,
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.assignedVariable`)}
>
<VarReferencePicker
readonly={readOnly}
nodeId={id}
isShowNodeName
value={inputs.assigned_variable_selector || []}
onChange={handleAssignedVarChanges}
filterVar={filterAssignedVar}
/>
</Field>
<Field
title={t(`${i18nPrefix}.writeMode`)}
>
<div className={cn('grid gap-2', isSupportAppend ? 'grid-cols-3' : 'grid-cols-2')}>
{writeModeTypes.map(type => (
<OptionCard
key={type}
title={t(`${i18nPrefix}.${type}`)}
onSelect={handleWriteModeChange(type)}
selected={inputs.write_mode === type}
/>
))}
</div>
</Field>
{inputs.write_mode !== WriteMode.Clear && (
<Field
title={t(`${i18nPrefix}.setVariable`)}
>
<VarReferencePicker
readonly={readOnly}
nodeId={id}
isShowNodeName
value={inputs.input_variable_selector || []}
onChange={handleToAssignedVarChange}
filterVar={filterToAssignedVar}
valueTypePlaceHolder={toAssignedVarType}
/>
</Field>
)}
</div>
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,13 @@
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
export enum WriteMode {
Overwrite = 'over-write',
Append = 'append',
Clear = 'clear',
}
export type AssignerNodeType = CommonNodeType & {
assigned_variable_selector: ValueSelector
write_mode: WriteMode
input_variable_selector: ValueSelector
}

View File

@@ -0,0 +1,150 @@
import { useCallback, useMemo } from 'react'
import produce from 'immer'
import { useStoreApi } from 'reactflow'
import { isEqual } from 'lodash-es'
import { VarType } from '../../types'
import type { ValueSelector, Var } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
useIsChatMode,
useNodesReadOnly,
useWorkflow,
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
const useConfig = (id: string, payload: AssignerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const {
getNodes,
} = store.getState()
const currentNode = getNodes().find(n => n.id === id)
const isInIteration = payload.isInIteration
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
const availableNodes = useMemo(() => {
return getBeforeNodesInSameBranch(id)
}, [getBeforeNodesInSameBranch, id])
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
const { getCurrentVariableType } = useWorkflowVariables()
const assignedVarType = getCurrentVariableType({
parentNode: iterationNode,
valueSelector: inputs.assigned_variable_selector || [],
availableNodes,
isChatMode,
isConstant: false,
})
const isSupportAppend = useCallback((varType: VarType) => {
return [VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varType)
}, [])
const isCurrSupportAppend = useMemo(() => isSupportAppend(assignedVarType), [assignedVarType, isSupportAppend])
const handleAssignedVarChanges = useCallback((variable: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.assigned_variable_selector = variable as ValueSelector
draft.input_variable_selector = []
const newVarType = getCurrentVariableType({
parentNode: iterationNode,
valueSelector: draft.assigned_variable_selector || [],
availableNodes,
isChatMode,
isConstant: false,
})
if (inputs.write_mode === WriteMode.Append && !isSupportAppend(newVarType))
draft.write_mode = WriteMode.Overwrite
})
setInputs(newInputs)
}, [inputs, setInputs, getCurrentVariableType, iterationNode, availableNodes, isChatMode, isSupportAppend])
const writeModeTypes = useMemo(() => {
const types = [WriteMode.Overwrite, WriteMode.Append, WriteMode.Clear]
if (!isSupportAppend(assignedVarType))
return types.filter(t => t !== WriteMode.Append)
return types
}, [assignedVarType, isSupportAppend])
const handleWriteModeChange = useCallback((writeMode: WriteMode) => {
return () => {
const newInputs = produce(inputs, (draft) => {
draft.write_mode = writeMode
if (inputs.write_mode === WriteMode.Clear)
draft.input_variable_selector = []
})
setInputs(newInputs)
}
}, [inputs, setInputs])
const toAssignedVarType = useMemo(() => {
const { write_mode } = inputs
if (write_mode === WriteMode.Overwrite)
return assignedVarType
if (write_mode === WriteMode.Append) {
if (assignedVarType === VarType.arrayString)
return VarType.string
if (assignedVarType === VarType.arrayNumber)
return VarType.number
if (assignedVarType === VarType.arrayObject)
return VarType.object
}
return VarType.string
}, [assignedVarType, inputs])
const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
return selector.join('.').startsWith('conversation')
}, [])
const filterToAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
if (isEqual(selector, inputs.assigned_variable_selector))
return false
if (inputs.write_mode === WriteMode.Overwrite) {
return varPayload.type === assignedVarType
}
else if (inputs.write_mode === WriteMode.Append) {
switch (assignedVarType) {
case VarType.arrayString:
return varPayload.type === VarType.string
case VarType.arrayNumber:
return varPayload.type === VarType.number
case VarType.arrayObject:
return varPayload.type === VarType.object
default:
return false
}
}
return true
}, [inputs.assigned_variable_selector, inputs.write_mode, assignedVarType])
const handleToAssignedVarChange = useCallback((value: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.input_variable_selector = value as ValueSelector
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
readOnly,
inputs,
handleAssignedVarChanges,
assignedVarType,
isSupportAppend: isCurrSupportAppend,
writeModeTypes,
handleWriteModeChange,
filterAssignedVar,
filterToAssignedVar,
handleToAssignedVarChange,
toAssignedVarType,
}
}
export default useConfig

View File

@@ -0,0 +1,5 @@
import type { AssignerNodeType } from './types'
export const checkNodeValid = (payload: AssignerNodeType) => {
return true
}

View File

@@ -24,6 +24,8 @@ import ToolNode from './tool/node'
import ToolPanel from './tool/panel'
import VariableAssignerNode from './variable-assigner/node'
import VariableAssignerPanel from './variable-assigner/panel'
import AssignerNode from './assigner/node'
import AssignerPanel from './assigner/panel'
import ParameterExtractorNode from './parameter-extractor/node'
import ParameterExtractorPanel from './parameter-extractor/panel'
import IterationNode from './iteration/node'
@@ -42,6 +44,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.HttpRequest]: HttpNode,
[BlockEnum.Tool]: ToolNode,
[BlockEnum.VariableAssigner]: VariableAssignerNode,
[BlockEnum.Assigner]: AssignerNode,
[BlockEnum.VariableAggregator]: VariableAssignerNode,
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
[BlockEnum.Iteration]: IterationNode,
@@ -61,6 +64,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Tool]: ToolPanel,
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
[BlockEnum.Assigner]: AssignerPanel,
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
[BlockEnum.Iteration]: IterationPanel,
}

View File

@@ -3,7 +3,7 @@ import React from 'react'
import cn from 'classnames'
import type { EndNodeType } from './types'
import type { NodeProps, Variable } from '@/app/components/workflow/types'
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import {
useIsChatMode,
useWorkflow,
@@ -12,7 +12,7 @@ import {
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { BlockEnum } from '@/app/components/workflow/types'
const Node: FC<NodeProps<EndNodeType>> = ({
@@ -44,6 +44,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
const node = getNode(value_selector[0])
const isSystem = isSystemVar(value_selector)
const isEnv = isENV(value_selector)
const isChatVar = isConversationVar(value_selector)
const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1]
const varType = getCurrentVariableType({
valueSelector: value_selector,
@@ -53,7 +54,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
return (
<div key={index} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
<div className='flex items-center text-xs font-medium text-gray-500'>
{!isEnv && (
{!isEnv && !isChatVar && (
<>
<div className='p-[1px]'>
<VarBlockIcon
@@ -66,9 +67,11 @@ const Node: FC<NodeProps<EndNodeType>> = ({
</>
)}
<div className='flex items-center text-primary-600'>
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', isEnv && '!max-w-[70px] text-gray-900')}>{varName}</div>
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!max-w-[70px] text-gray-900')}>{varName}</div>
</div>
</div>
<div className='text-xs font-normal text-gray-700'>

View File

@@ -17,6 +17,8 @@ type Props = {
onChange: (newList: KeyValue[]) => void
onAdd: () => void
// onSwitchToBulkEdit: () => void
keyNotSupportVar?: boolean
insertVarTipToLeft?: boolean
}
const KeyValueList: FC<Props> = ({
@@ -26,6 +28,8 @@ const KeyValueList: FC<Props> = ({
onChange,
onAdd,
// onSwitchToBulkEdit,
keyNotSupportVar,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()
@@ -47,6 +51,9 @@ const KeyValueList: FC<Props> = ({
}
}, [list, onChange])
if (!Array.isArray(list))
return null
return (
<div className='border border-gray-200 rounded-lg overflow-hidden'>
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
@@ -79,6 +86,8 @@ const KeyValueList: FC<Props> = ({
onAdd={onAdd}
readonly={readonly}
canRemove={list.length > 1}
keyNotSupportVar={keyNotSupportVar}
insertVarTipToLeft={insertVarTipToLeft}
/>
))
}

View File

@@ -18,6 +18,7 @@ type Props = {
onRemove?: () => void
placeholder?: string
readOnly?: boolean
insertVarTipToLeft?: boolean
}
const InputItem: FC<Props> = ({
@@ -30,6 +31,7 @@ const InputItem: FC<Props> = ({
onRemove,
placeholder,
readOnly,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()
@@ -64,6 +66,7 @@ const InputItem: FC<Props> = ({
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
promptMinHeightClassName='h-full'
insertVarTipToLeft={insertVarTipToLeft}
/>
)
: <div
@@ -83,6 +86,7 @@ const InputItem: FC<Props> = ({
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
promptMinHeightClassName='h-full'
insertVarTipToLeft={insertVarTipToLeft}
/>
)}

View File

@@ -6,6 +6,7 @@ import produce from 'immer'
import type { KeyValue } from '../../../types'
import InputItem from './input-item'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
const i18nPrefix = 'workflow.nodes.http'
@@ -20,6 +21,8 @@ type Props = {
onRemove: () => void
isLastItem: boolean
onAdd: () => void
keyNotSupportVar?: boolean
insertVarTipToLeft?: boolean
}
const KeyValueItem: FC<Props> = ({
@@ -33,6 +36,8 @@ const KeyValueItem: FC<Props> = ({
onRemove,
isLastItem,
onAdd,
keyNotSupportVar,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()
@@ -51,15 +56,26 @@ const KeyValueItem: FC<Props> = ({
// group class name is for hover row show remove button
<div className={cn(className, 'group flex h-min-7 border-t border-gray-200')}>
<div className='w-1/2 border-r border-gray-200'>
<InputItem
instanceId={`http-key-${instanceId}`}
nodeId={nodeId}
value={payload.key}
onChange={handleChange('key')}
hasRemove={false}
placeholder={t(`${i18nPrefix}.key`)!}
readOnly={readonly}
/>
{!keyNotSupportVar
? (
<InputItem
instanceId={`http-key-${instanceId}`}
nodeId={nodeId}
value={payload.key}
onChange={handleChange('key')}
hasRemove={false}
placeholder={t(`${i18nPrefix}.key`)!}
readOnly={readonly}
insertVarTipToLeft={insertVarTipToLeft}
/>
)
: (
<Input
className='rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
value={payload.key}
onChange={handleChange('key')}
/>
)}
</div>
<div className='w-1/2'>
<InputItem
@@ -71,6 +87,7 @@ const KeyValueItem: FC<Props> = ({
onRemove={onRemove}
placeholder={t(`${i18nPrefix}.value`)!}
readOnly={readonly}
insertVarTipToLeft={insertVarTipToLeft}
/>
</div>
</div>

View File

@@ -19,7 +19,7 @@ import {
import { filterVar } from '../utils'
import AddVariable from './add-variable'
import NodeVariableItem from './node-variable-item'
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.variableAssigner'
@@ -124,6 +124,8 @@ const NodeGroupItem = ({
!!item.variables.length && item.variables.map((variable = [], index) => {
const isSystem = isSystemVar(variable)
const isEnv = isENV(variable)
const isChatVar = isConversationVar(variable)
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
@@ -131,6 +133,7 @@ const NodeGroupItem = ({
<NodeVariableItem
key={index}
isEnv={isEnv}
isChatVar={isChatVar}
node={node as Node}
varName={varName}
showBorder={showSelectedBorder || showSelectionBorder}

View File

@@ -3,28 +3,33 @@ import cn from '@/utils/classnames'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
type NodeVariableItemProps = {
isEnv: boolean
isChatVar: boolean
node: Node
varName: string
showBorder?: boolean
className?: string
}
const NodeVariableItem = ({
isEnv,
isChatVar,
node,
varName,
showBorder,
className,
}: NodeVariableItemProps) => {
return (
<div className={cn(
'relative flex items-center mt-0.5 h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700',
showBorder && '!bg-black/[0.02]',
className,
)}>
{!isEnv && (
{!isEnv && !isChatVar && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
@@ -37,9 +42,10 @@ const NodeVariableItem = ({
</div>
)}
<div className='flex items-center text-primary-600'>
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
<div className={cn('max-w-[75px] truncate ml-0.5 text-xs font-medium', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('max-w-[75px] truncate ml-0.5 text-xs font-medium', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
</div>
</div>
)

View File

@@ -143,6 +143,7 @@ export const useGetAvailableVars = () => {
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
isChatMode,
hideEnv,
hideChatVar: hideEnv,
filterVar,
})
.map(node => ({

View File

@@ -0,0 +1,72 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import produce from 'immer'
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
import Button from '@/app/components/base/button'
type Props = {
isString: boolean
list: any[]
onChange: (list: any[]) => void
}
const ArrayValueList: FC<Props> = ({
isString = true,
list,
onChange,
}) => {
const { t } = useTranslation()
const handleNameChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => {
const newList = produce(list, (draft: any[]) => {
draft[index] = isString ? e.target.value : Number(e.target.value)
})
onChange(newList)
}
}, [isString, list, onChange])
const handleItemRemove = useCallback((index: number) => {
return () => {
const newList = produce(list, (draft) => {
draft.splice(index, 1)
})
onChange(newList)
}
}, [list, onChange])
const handleItemAdd = useCallback(() => {
const newList = produce(list, (draft: any[]) => {
draft.push(undefined)
})
onChange(newList)
}, [list, onChange])
return (
<div className='w-full space-y-2'>
{list.map((item, index) => (
<div className='flex items-center space-x-1' key={index}>
<input
className='block px-3 w-full h-8 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.chatVariable.modal.arrayValue') || ''}
value={list[index]}
onChange={handleNameChange(index)}
type={isString ? 'text' : 'number'}
/>
<RemoveButton
className='!p-2 !bg-gray-100 hover:!bg-gray-200'
onClick={handleItemRemove(index)}
/>
</div>
))}
<Button variant='tertiary' className='w-full' onClick={handleItemAdd}>
<RiAddLine className='mr-1 w-4 h-4' />
<span>{t('workflow.chatVariable.modal.addArrayValue')}</span>
</Button>
</div>
)
}
export default React.memo(ArrayValueList)

View File

@@ -0,0 +1,135 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { useContext } from 'use-context-selector'
import { ToastContext } from '@/app/components/base/toast'
import VariableTypeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/variable-type-select'
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
type Props = {
index: number
list: any[]
onChange: (list: any[]) => void
}
const typeList = [
ChatVarType.String,
ChatVarType.Number,
]
export const DEFAULT_OBJECT_VALUE = {
key: '',
type: ChatVarType.String,
value: undefined,
}
const ObjectValueItem: FC<Props> = ({
index,
list,
onChange,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [isFocus, setIsFocus] = useState(false)
const handleKeyChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => {
const newList = produce(list, (draft: any[]) => {
if (!/^[a-zA-Z0-9_]+$/.test(e.target.value))
return notify({ type: 'error', message: 'key is can only contain letters, numbers and underscores' })
draft[index].key = e.target.value
})
onChange(newList)
}
}, [list, notify, onChange])
const handleTypeChange = useCallback((index: number) => {
return (type: ChatVarType) => {
const newList = produce(list, (draft) => {
draft[index].type = type
if (type === ChatVarType.Number)
draft[index].value = isNaN(Number(draft[index].value)) ? undefined : Number(draft[index].value)
else
draft[index].value = draft[index].value ? String(draft[index].value) : undefined
})
onChange(newList)
}
}, [list, onChange])
const handleValueChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => {
const newList = produce(list, (draft: any[]) => {
draft[index].value = draft[index].type === ChatVarType.String ? e.target.value : isNaN(Number(e.target.value)) ? undefined : Number(e.target.value)
})
onChange(newList)
}
}, [list, onChange])
const handleItemRemove = useCallback((index: number) => {
return () => {
const newList = produce(list, (draft) => {
draft.splice(index, 1)
})
onChange(newList)
}
}, [list, onChange])
const handleItemAdd = useCallback(() => {
const newList = produce(list, (draft: any[]) => {
draft.push(DEFAULT_OBJECT_VALUE)
})
onChange(newList)
}, [list, onChange])
const handleFocusChange = useCallback(() => {
setIsFocus(true)
if (index === list.length - 1)
handleItemAdd()
}, [handleItemAdd, index, list.length])
return (
<div className='group flex border-t border-gray-200'>
{/* Key */}
<div className='w-[120px] border-r border-gray-200'>
<input
className='block px-2 w-full h-7 text-text-secondary system-xs-regular appearance-none outline-none caret-primary-600 hover:bg-state-base-hover focus:bg-components-input-bg-active placeholder:system-xs-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.chatVariable.modal.objectKey') || ''}
value={list[index].key}
onChange={handleKeyChange(index)}
/>
</div>
{/* Type */}
<div className='w-[96px] border-r border-gray-200'>
<VariableTypeSelector
inCell
value={list[index].type}
list={typeList}
onSelect={handleTypeChange(index)}
popupClassName='w-[120px]'
/>
</div>
{/* Value */}
<div className='relative w-[230px]'>
<input
className='block px-2 w-full h-7 text-text-secondary system-xs-regular appearance-none outline-none caret-primary-600 hover:bg-state-base-hover focus:bg-components-input-bg-active placeholder:system-xs-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.chatVariable.modal.objectValue') || ''}
value={list[index].value}
onChange={handleValueChange(index)}
onFocus={() => handleFocusChange()}
onBlur={() => setIsFocus(false)}
type={list[index].type === ChatVarType.Number ? 'number' : 'text'}
/>
{list.length > 1 && !isFocus && (
<RemoveButton
className='z-10 group-hover:block hidden absolute right-1 top-0.5'
onClick={handleItemRemove(index)}
/>
)}
</div>
</div>
)
}
export default React.memo(ObjectValueItem)

View File

@@ -0,0 +1,36 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import ObjectValueItem from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-item'
type Props = {
list: any[]
onChange: (list: any[]) => void
}
const ObjectValueList: FC<Props> = ({
list,
onChange,
}) => {
const { t } = useTranslation()
return (
<div className='w-full border border-gray-200 rounded-lg overflow-hidden'>
<div className='flex items-center h-7 system-xs-medium text-text-tertiary uppercase'>
<div className='w-[120px] flex items-center h-full pl-2 border-r border-gray-200'>{t('workflow.chatVariable.modal.objectKey')}</div>
<div className='w-[96px] flex items-center h-full pl-2 border-r border-gray-200'>{t('workflow.chatVariable.modal.objectType')}</div>
<div className='w-[230px] flex items-center h-full pl-2 pr-1'>{t('workflow.chatVariable.modal.objectValue')}</div>
</div>
{list.map((item, index) => (
<ObjectValueItem
key={index}
index={index}
list={list}
onChange={onChange}
/>
))}
</div>
)
}
export default React.memo(ObjectValueList)

View File

@@ -0,0 +1,49 @@
import { memo, useState } from 'react'
import { capitalize } from 'lodash-es'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import type { ConversationVariable } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type VariableItemProps = {
item: ConversationVariable
onEdit: (item: ConversationVariable) => void
onDelete: (item: ConversationVariable) => void
}
const VariableItem = ({
item,
onEdit,
onDelete,
}: VariableItemProps) => {
const [destructive, setDestructive] = useState(false)
return (
<div className={cn(
'mb-1 px-2.5 py-2 bg-components-panel-on-panel-item-bg radius-md border border-components-panel-border-subtle shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
)}>
<div className='flex items-center justify-between'>
<div className='grow flex gap-1 items-center'>
<BubbleX className='w-4 h-4 text-util-colors-teal-teal-700' />
<div className='text-text-primary system-sm-medium'>{item.name}</div>
<div className='text-text-tertiary system-xs-medium'>{capitalize(item.value_type)}</div>
</div>
<div className='shrink-0 flex gap-1 items-center text-text-tertiary'>
<div className='p-1 radius-md cursor-pointer hover:bg-state-base-hover hover:text-text-secondary'>
<RiEditLine className='w-4 h-4' onClick={() => onEdit(item)}/>
</div>
<div
className='p-1 radius-md cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive'
onMouseOver={() => setDestructive(true)}
onMouseOut={() => setDestructive(false)}
>
<RiDeleteBinLine className='w-4 h-4' onClick={() => onDelete(item)}/>
</div>
</div>
</div>
<div className='text-text-tertiary system-xs-regular truncate'>{item.description}</div>
</div>
)
}
export default memo(VariableItem)

View File

@@ -0,0 +1,69 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { ConversationVariable } from '@/app/components/workflow/types'
type Props = {
open: boolean
setOpen: (value: React.SetStateAction<boolean>) => void
showTip: boolean
chatVar?: ConversationVariable
onClose: () => void
onSave: (env: ConversationVariable) => void
}
const VariableModalTrigger = ({
open,
setOpen,
showTip,
chatVar,
onClose,
onSave,
}: Props) => {
const { t } = useTranslation()
return (
<PortalToFollowElem
open={open}
onOpenChange={() => {
setOpen(v => !v)
open && onClose()
}}
placement='left-start'
offset={{
mainAxis: 8,
alignmentAxis: showTip ? -278 : -48,
}}
>
<PortalToFollowElemTrigger onClick={() => {
setOpen(v => !v)
open && onClose()
}}>
<Button variant='primary'>
<RiAddLine className='mr-1 w-4 h-4' />
<span className='system-sm-medium'>{t('workflow.chatVariable.button')}</span>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<VariableModal
chatVar={chatVar}
onSave={onSave}
onClose={() => {
onClose()
setOpen(false)
}}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default VariableModalTrigger

View File

@@ -0,0 +1,388 @@
import React, { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { v4 as uuid4 } from 'uuid'
import { RiCloseLine, RiDraftLine, RiInputField } from '@remixicon/react'
import VariableTypeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/variable-type-select'
import ObjectValueList from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-list'
import { DEFAULT_OBJECT_VALUE } from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-item'
import ArrayValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-value-list'
import Button from '@/app/components/base/button'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { ToastContext } from '@/app/components/base/toast'
import { useStore } from '@/app/components/workflow/store'
import type { ConversationVariable } from '@/app/components/workflow/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import cn from '@/utils/classnames'
export type ModalPropsType = {
chatVar?: ConversationVariable
onClose: () => void
onSave: (chatVar: ConversationVariable) => void
}
type ObjectValueItem = {
key: string
type: ChatVarType
value: string | number | undefined
}
const typeList = [
ChatVarType.String,
ChatVarType.Number,
ChatVarType.Object,
ChatVarType.ArrayString,
ChatVarType.ArrayNumber,
ChatVarType.ArrayObject,
]
const objectPlaceholder = `# example
# {
# "name": "ray",
# "age": 20
# }`
const arrayStringPlaceholder = `# example
# [
# "value1",
# "value2"
# ]`
const arrayNumberPlaceholder = `# example
# [
# 100,
# 200
# ]`
const arrayObjectPlaceholder = `# example
# [
# {
# "name": "ray",
# "age": 20
# },
# {
# "name": "lily",
# "age": 18
# }
# ]`
const ChatVariableModal = ({
chatVar,
onClose,
onSave,
}: ModalPropsType) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const varList = useStore(s => s.conversationVariables)
const [name, setName] = React.useState('')
const [type, setType] = React.useState<ChatVarType>(ChatVarType.String)
const [value, setValue] = React.useState<any>()
const [objectValue, setObjectValue] = React.useState<ObjectValueItem[]>([DEFAULT_OBJECT_VALUE])
const [editorContent, setEditorContent] = React.useState<string>()
const [editInJSON, setEditInJSON] = React.useState(false)
const [des, setDes] = React.useState<string>('')
const editorMinHeight = useMemo(() => {
if (type === ChatVarType.ArrayObject)
return '240px'
return '120px'
}, [type])
const placeholder = useMemo(() => {
if (type === ChatVarType.ArrayString)
return arrayStringPlaceholder
if (type === ChatVarType.ArrayNumber)
return arrayNumberPlaceholder
if (type === ChatVarType.ArrayObject)
return arrayObjectPlaceholder
return objectPlaceholder
}, [type])
const getObjectValue = useCallback(() => {
if (!chatVar)
return [DEFAULT_OBJECT_VALUE]
return Object.keys(chatVar.value).map((key) => {
return {
key,
type: typeof chatVar.value[key] === 'string' ? ChatVarType.String : ChatVarType.Number,
value: chatVar.value[key],
}
})
}, [chatVar])
const formatValueFromObject = useCallback((list: ObjectValueItem[]) => {
return list.reduce((acc: any, curr) => {
if (curr.key)
acc[curr.key] = curr.value || null
return acc
}, {})
}, [])
const formatValue = (value: any) => {
switch (type) {
case ChatVarType.String:
return value || ''
case ChatVarType.Number:
return value || 0
case ChatVarType.Object:
return formatValueFromObject(objectValue)
case ChatVarType.ArrayString:
case ChatVarType.ArrayNumber:
case ChatVarType.ArrayObject:
return value?.filter(Boolean) || []
}
}
const handleNameChange = (v: string) => {
if (!v)
return setName('')
if (!/^[a-zA-Z0-9_]+$/.test(v))
return notify({ type: 'error', message: 'name is can only contain letters, numbers and underscores' })
if (/^[0-9]/.test(v))
return notify({ type: 'error', message: 'name can not start with a number' })
setName(v)
}
const handleTypeChange = (v: ChatVarType) => {
setValue(undefined)
setEditorContent(undefined)
if (v === ChatVarType.ArrayObject)
setEditInJSON(true)
if (v === ChatVarType.String || v === ChatVarType.Number || v === ChatVarType.Object)
setEditInJSON(false)
setType(v)
}
const handleEditorChange = (editInJSON: boolean) => {
if (type === ChatVarType.Object) {
if (editInJSON) {
const newValue = !objectValue[0].key ? undefined : formatValueFromObject(objectValue)
setValue(newValue)
setEditorContent(JSON.stringify(newValue))
}
else {
if (!editorContent) {
setValue(undefined)
setObjectValue([DEFAULT_OBJECT_VALUE])
}
else {
try {
const newValue = JSON.parse(editorContent)
setValue(newValue)
const newObjectValue = Object.keys(newValue).map((key) => {
return {
key,
type: typeof newValue[key] === 'string' ? ChatVarType.String : ChatVarType.Number,
value: newValue[key],
}
})
setObjectValue(newObjectValue)
}
catch (e) {
// ignore JSON.parse errors
}
}
}
}
if (type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) {
if (editInJSON) {
const newValue = (value?.length && value.filter(Boolean).length) ? value.filter(Boolean) : undefined
setValue(newValue)
if (!editorContent)
setEditorContent(JSON.stringify(newValue))
}
else {
setValue(value?.length ? value : [undefined])
}
}
setEditInJSON(editInJSON)
}
const handleEditorValueChange = (content: string) => {
if (!content) {
setEditorContent(content)
return setValue(undefined)
}
else {
setEditorContent(content)
try {
const newValue = JSON.parse(content)
setValue(newValue)
}
catch (e) {
// ignore JSON.parse errors
}
}
}
const handleSave = () => {
if (!name)
return notify({ type: 'error', message: 'name can not be empty' })
if (!chatVar && varList.some(chatVar => chatVar.name === name))
return notify({ type: 'error', message: 'name is existed' })
// if (type !== ChatVarType.Object && !value)
// return notify({ type: 'error', message: 'value can not be empty' })
if (type === ChatVarType.Object && objectValue.some(item => !item.key && !!item.value))
return notify({ type: 'error', message: 'object key can not be empty' })
onSave({
id: chatVar ? chatVar.id : uuid4(),
name,
value_type: type,
value: formatValue(value),
description: des,
})
onClose()
}
useEffect(() => {
if (chatVar) {
setName(chatVar.name)
setType(chatVar.value_type)
setValue(chatVar.value)
setDes(chatVar.description)
setEditInJSON(false)
setObjectValue(getObjectValue())
}
}, [chatVar, getObjectValue])
return (
<div
className={cn('flex flex-col w-[360px] bg-components-panel-bg rounded-2xl h-full border-[0.5px] border-components-panel-border shadow-2xl', type === ChatVarType.Object && 'w-[480px]')}
>
<div className='shrink-0 flex items-center justify-between mb-3 p-4 pb-0 text-text-primary system-xl-semibold'>
{!chatVar ? t('workflow.chatVariable.modal.title') : t('workflow.chatVariable.modal.editTitle')}
<div className='flex items-center'>
<div
className='flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={onClose}
>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div>
</div>
</div>
<div className='px-4 py-2 max-h-[480px] overflow-y-auto'>
{/* name */}
<div className='mb-4'>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold'>{t('workflow.chatVariable.modal.name')}</div>
<div className='flex'>
<input
tabIndex={0}
className='block px-3 w-full h-8 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.chatVariable.modal.namePlaceholder') || ''}
value={name}
onChange={e => handleNameChange(e.target.value)}
type='text'
/>
</div>
</div>
{/* type */}
<div className='mb-4'>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold'>{t('workflow.chatVariable.modal.type')}</div>
<div className='flex'>
<VariableTypeSelector
value={type}
list={typeList}
onSelect={handleTypeChange}
popupClassName='w-[327px]'
/>
</div>
</div>
{/* default value */}
<div className='mb-4'>
<div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold'>
<div>{t('workflow.chatVariable.modal.value')}</div>
{(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) && (
<Button
variant='ghost'
size='small'
className='text-text-tertiary'
onClick={() => handleEditorChange(!editInJSON)}
>
{editInJSON ? <RiInputField className='mr-1 w-3.5 h-3.5' /> : <RiDraftLine className='mr-1 w-3.5 h-3.5' />}
{editInJSON ? t('workflow.chatVariable.modal.oneByOne') : t('workflow.chatVariable.modal.editInJSON')}
</Button>
)}
{type === ChatVarType.Object && (
<Button
variant='ghost'
size='small'
className='text-text-tertiary'
onClick={() => handleEditorChange(!editInJSON)}
>
{editInJSON ? <RiInputField className='mr-1 w-3.5 h-3.5' /> : <RiDraftLine className='mr-1 w-3.5 h-3.5' />}
{editInJSON ? t('workflow.chatVariable.modal.editInForm') : t('workflow.chatVariable.modal.editInJSON')}
</Button>
)}
</div>
<div className='flex'>
{type === ChatVarType.String && (
<input
className='block px-3 w-full h-8 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''}
value={value}
onChange={e => setValue(e.target.value)}
/>
)}
{type === ChatVarType.Number && (
<input
className='block px-3 w-full h-8 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''}
value={value}
onChange={e => setValue(Number(e.target.value))}
type='number'
/>
)}
{type === ChatVarType.Object && !editInJSON && (
<ObjectValueList
list={objectValue}
onChange={setObjectValue}
/>
)}
{type === ChatVarType.ArrayString && !editInJSON && (
<ArrayValueList
isString
list={value || [undefined]}
onChange={setValue}
/>
)}
{type === ChatVarType.ArrayNumber && !editInJSON && (
<ArrayValueList
isString={false}
list={value || [undefined]}
onChange={setValue}
/>
)}
{editInJSON && (
<div className='w-full py-2 pl-3 pr-1 rounded-[10px] bg-components-input-bg-normal' style={{ height: editorMinHeight }}>
<CodeEditor
isExpand
noWrapper
language={CodeLanguage.json}
value={editorContent}
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
onChange={handleEditorValueChange}
/>
</div>
)}
</div>
</div>
{/* description */}
<div className=''>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold'>{t('workflow.chatVariable.modal.description')}</div>
<div className='flex'>
<textarea
className='block p-2 w-full h-20 rounded-lg bg-components-input-bg-normal border border-transparent system-sm-regular outline-none appearance-none caret-primary-600 resize-none hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
value={des}
placeholder={t('workflow.chatVariable.modal.descriptionPlaceholder') || ''}
onChange={e => setDes(e.target.value)}
/>
</div>
</div>
</div>
<div className='p-4 pt-2 flex flex-row-reverse rounded-b-2xl'>
<div className='flex gap-2'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
</div>
)
}
export default ChatVariableModal

View File

@@ -0,0 +1,66 @@
'use client'
import React, { useState } from 'react'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
type Props = {
inCell?: boolean
value?: any
list: any
onSelect: (value: any) => void
popupClassName?: string
}
const VariableTypeSelector = ({
inCell = false,
value,
list,
onSelect,
popupClassName,
}: Props) => {
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={() => setOpen(v => !v)}
placement='bottom'
>
<PortalToFollowElemTrigger className='w-full' onClick={() => setOpen(v => !v)}>
<div className={cn(
'flex items-center w-full px-2 cursor-pointer',
!inCell && 'py-1 bg-components-input-bg-normal hover:bg-state-base-hover-alt radius-md',
inCell && 'py-0.5 hover:bg-state-base-hover',
open && !inCell && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
open && inCell && 'bg-state-base-hover hover:bg-state-base-hover',
)}>
<div className={cn(
'grow p-1 system-sm-regular text-components-input-text-filled truncate',
inCell && 'system-xs-regular text-text-secondary',
)}>{value}</div>
<RiArrowDownSLine className='ml-0.5 w-4 h-4 text-text-quaternary' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn('w-full z-[11]', popupClassName)}>
<div className='p-1 bg-components-panel-bg-blur border-[0.5px] border-components-panel-border radius-xl shadow-lg'>
{list.map((item: any) => (
<div key={item} className='flex items-center gap-2 pl-3 pr-2 py-[6px] radius-md cursor-pointer hover:bg-state-base-hover' onClick={() => {
onSelect(item)
setOpen(false)
}}>
<div className='grow system-md-regular text-text-secondary truncate'>{item}</div>
{value === item && <RiCheckLine className='w-4 h-4 text-text-accent' />}
</div>
))}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default VariableTypeSelector

View File

@@ -0,0 +1,202 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useContext } from 'use-context-selector'
import {
useStoreApi,
} from 'reactflow'
import { RiBookOpenLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useStore } from '@/app/components/workflow/store'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { BubbleX, LongArrowLeft, LongArrowRight } from '@/app/components/base/icons/src/vender/line/others'
import BlockIcon from '@/app/components/workflow/block-icon'
import VariableModalTrigger from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger'
import VariableItem from '@/app/components/workflow/panel/chat-variable-panel/components/variable-item'
import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm'
import type {
ConversationVariable,
} from '@/app/components/workflow/types'
import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import { BlockEnum } from '@/app/components/workflow/types'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import cn from '@/utils/classnames'
const ChatVariablePanel = () => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const store = useStoreApi()
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
const updateChatVarList = useStore(s => s.setConversationVariables)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const [showTip, setShowTip] = useState(true)
const [showVariableModal, setShowVariableModal] = useState(false)
const [currentVar, setCurrentVar] = useState<ConversationVariable>()
const [showRemoveVarConfirm, setShowRemoveConfirm] = useState(false)
const [cacheForDelete, setCacheForDelete] = useState<ConversationVariable>()
const getEffectedNodes = useCallback((chatVar: ConversationVariable) => {
const { getNodes } = store.getState()
const allNodes = getNodes()
return findUsedVarNodes(
['conversation', chatVar.name],
allNodes,
)
}, [store])
const removeUsedVarInNodes = useCallback((chatVar: ConversationVariable) => {
const { getNodes, setNodes } = store.getState()
const effectedNodes = getEffectedNodes(chatVar)
const newNodes = getNodes().map((node) => {
if (effectedNodes.find(n => n.id === node.id))
return updateNodeVars(node, ['conversation', chatVar.name], [])
return node
})
setNodes(newNodes)
}, [getEffectedNodes, store])
const handleEdit = (chatVar: ConversationVariable) => {
setCurrentVar(chatVar)
setShowVariableModal(true)
}
const handleDelete = useCallback((chatVar: ConversationVariable) => {
removeUsedVarInNodes(chatVar)
updateChatVarList(varList.filter(v => v.id !== chatVar.id))
setCacheForDelete(undefined)
setShowRemoveConfirm(false)
doSyncWorkflowDraft()
}, [doSyncWorkflowDraft, removeUsedVarInNodes, updateChatVarList, varList])
const deleteCheck = useCallback((chatVar: ConversationVariable) => {
const effectedNodes = getEffectedNodes(chatVar)
if (effectedNodes.length > 0) {
setCacheForDelete(chatVar)
setShowRemoveConfirm(true)
}
else {
handleDelete(chatVar)
}
}, [getEffectedNodes, handleDelete])
const handleSave = useCallback(async (chatVar: ConversationVariable) => {
// add chatVar
if (!currentVar) {
const newList = [chatVar, ...varList]
updateChatVarList(newList)
doSyncWorkflowDraft()
return
}
// edit chatVar
const newList = varList.map(v => v.id === currentVar.id ? chatVar : v)
updateChatVarList(newList)
// side effects of rename env
if (currentVar.name !== chatVar.name) {
const { getNodes, setNodes } = store.getState()
const effectedNodes = getEffectedNodes(currentVar)
const newNodes = getNodes().map((node) => {
if (effectedNodes.find(n => n.id === node.id))
return updateNodeVars(node, ['conversation', currentVar.name], ['conversation', chatVar.name])
return node
})
setNodes(newNodes)
}
doSyncWorkflowDraft()
}, [currentVar, doSyncWorkflowDraft, getEffectedNodes, store, updateChatVarList, varList])
return (
<div
className={cn(
'relative flex flex-col w-[420px] bg-components-panel-bg-alt rounded-l-2xl h-full border border-components-panel-border',
)}
>
<div className='shrink-0 flex items-center justify-between p-4 pb-0 text-text-primary system-xl-semibold'>
{t('workflow.chatVariable.panelTitle')}
<div className='flex items-center gap-1'>
<ActionButton state={showTip ? ActionButtonState.Active : undefined} onClick={() => setShowTip(!showTip)}>
<RiBookOpenLine className='w-4 h-4' />
</ActionButton>
<div
className='flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={() => setShowChatVariablePanel(false)}
>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div>
</div>
</div>
{showTip && (
<div className='shrink-0 px-3 pt-2.5 pb-2'>
<div className='relative p-3 radius-2xl bg-background-section-burn'>
<div className='inline-block py-[3px] px-[5px] rounded-[5px] border border-divider-deep text-text-tertiary system-2xs-medium-uppercase'>TIPS</div>
<div className='mt-1 mb-4 system-sm-regular text-text-secondary'>
{t('workflow.chatVariable.panelDescription')}
<a target='_blank' rel='noopener noreferrer' className='text-text-accent' href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/guides/workflow/key_concepts#conversation-variables' : `https://docs.dify.ai/v/${locale.toLowerCase()}/guides/workflow/key_concept#hui-hua-bian-liang`}>{t('workflow.chatVariable.docLink')}</a>
</div>
<div className='flex items-center gap-2'>
<div className='flex flex-col p-3 pb-4 bg-workflow-block-bg radius-lg border border-workflow-block-border shadow-md'>
<BubbleX className='shrink-0 mb-1 w-4 h-4 text-util-colors-teal-teal-700' />
<div className='text-text-secondary system-xs-semibold'>conversation_var</div>
<div className='text-text-tertiary system-2xs-regular'>String</div>
</div>
<div className='grow'>
<div className='mb-2 flex items-center gap-2 py-1'>
<div className='shrink-0 flex items-center gap-1 w-16 h-3 px-1'>
<LongArrowLeft className='grow h-2 text-text-quaternary' />
<div className='shrink-0 text-text-tertiary system-2xs-medium'>WRITE</div>
</div>
<BlockIcon className='shrink-0' type={BlockEnum.Assigner} />
<div className='grow text-text-secondary system-xs-semibold truncate'>{t('workflow.blocks.assigner')}</div>
</div>
<div className='flex items-center gap-2 py-1'>
<div className='shrink-0 flex items-center gap-1 w-16 h-3 px-1'>
<div className='shrink-0 text-text-tertiary system-2xs-medium'>READ</div>
<LongArrowRight className='grow h-2 text-text-quaternary' />
</div>
<BlockIcon className='shrink-0' type={BlockEnum.LLM} />
<div className='grow text-text-secondary system-xs-semibold truncate'>{t('workflow.blocks.llm')}</div>
</div>
</div>
</div>
<div className='absolute z-10 top-[-4px] right-[38px] w-3 h-3 bg-background-section-burn rotate-45'/>
</div>
</div>
)}
<div className='shrink-0 px-4 pt-2 pb-3'>
<VariableModalTrigger
open={showVariableModal}
setOpen={setShowVariableModal}
showTip={showTip}
chatVar={currentVar}
onSave={handleSave}
onClose={() => setCurrentVar(undefined)}
/>
</div>
<div className='grow px-4 rounded-b-2xl overflow-y-auto'>
{varList.map(chatVar => (
<VariableItem
key={chatVar.id}
item={chatVar}
onEdit={handleEdit}
onDelete={deleteCheck}
/>
))}
</div>
<RemoveEffectVarConfirm
isShow={showRemoveVarConfirm}
onCancel={() => setShowRemoveConfirm(false)}
onConfirm={() => cacheForDelete && handleDelete(cacheForDelete)}
/>
</div>
)
}
export default memo(ChatVariablePanel)

View File

@@ -0,0 +1,8 @@
export enum ChatVarType {
Number = 'number',
String = 'string',
Object = 'object',
ArrayString = 'array[string]',
ArrayNumber = 'array[number]',
ArrayObject = 'array[object]',
}

View File

@@ -14,6 +14,7 @@ import {
import type { StartNodeType } from '../../nodes/start/types'
import Empty from './empty'
import UserInput from './user-input'
import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
@@ -25,7 +26,13 @@ import {
} from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
type ChatWrapperProps = {
showConversationVariableModal: boolean
onConversationModalHide: () => void
showInputsFieldsPanel: boolean
}
const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConversationVariableModal, onConversationModalHide, showInputsFieldsPanel }, ref) => {
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables
@@ -87,33 +94,41 @@ const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
}, [handleRestart])
return (
<Chat
config={{
...config,
supportCitationHitInfo: true,
} as any}
chatList={chatList}
isResponding={isResponding}
chatContainerClassName='px-4'
chatContainerInnerClassName='pt-6'
chatFooterClassName='px-4 rounded-bl-2xl'
chatFooterInnerClassName='pb-4'
onSend={doSend}
onStopResponding={handleStop}
chatNode={(
<>
<UserInput />
{
!chatList.length && (
<Empty />
)
}
</>
<>
<Chat
config={{
...config,
supportCitationHitInfo: true,
} as any}
chatList={chatList}
isResponding={isResponding}
chatContainerClassName='px-3'
chatContainerInnerClassName='pt-6'
chatFooterClassName='px-4 rounded-bl-2xl'
chatFooterInnerClassName='pb-4'
onSend={doSend}
onStopResponding={handleStop}
chatNode={(
<>
{showInputsFieldsPanel && <UserInput />}
{
!chatList.length && (
<Empty />
)
}
</>
)}
suggestedQuestions={suggestedQuestions}
showPromptLog
chatAnswerContainerInner='!pr-2'
/>
{showConversationVariableModal && (
<ConversationVariableModal
conversationID={conversationId}
onHide={onConversationModalHide}
/>
)}
suggestedQuestions={suggestedQuestions}
showPromptLog
chatAnswerContainerInner='!pr-2'
/>
</>
)
})

View File

@@ -0,0 +1,155 @@
'use client'
import React, { useCallback } from 'react'
import { useMount } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { capitalize } from 'lodash-es'
import copy from 'copy-to-clipboard'
import { RiCloseLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import {
Clipboard,
ClipboardCheck,
} from '@/app/components/base/icons/src/vender/line/files'
import { useStore } from '@/app/components/workflow/store'
import type {
ConversationVariable,
} from '@/app/components/workflow/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import useTimestamp from '@/hooks/use-timestamp'
import { fetchCurrentValueOfConversationVariable } from '@/service/workflow'
import cn from '@/utils/classnames'
export type Props = {
conversationID: string
onHide: () => void
}
const ConversationVariableModal = ({
conversationID,
onHide,
}: Props) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
const appID = useStore(s => s.appId)
const [currentVar, setCurrentVar] = React.useState<ConversationVariable>(varList[0])
const [latestValueMap, setLatestValueMap] = React.useState<Record<string, string>>({})
const [latestValueTimestampMap, setLatestValueTimestampMap] = React.useState<Record<string, number>>({})
const getChatVarLatestValues = useCallback(async () => {
if (conversationID && varList.length > 0) {
const res = await fetchCurrentValueOfConversationVariable({
url: `/apps/${appID}/conversation-variables`,
params: { conversation_id: conversationID },
})
if (res.data.length > 0) {
const valueMap = res.data.reduce((acc: any, cur) => {
acc[cur.id] = cur.value
return acc
}, {})
setLatestValueMap(valueMap)
const timestampMap = res.data.reduce((acc: any, cur) => {
acc[cur.id] = cur.updated_at
return acc
}, {})
setLatestValueTimestampMap(timestampMap)
}
}
}, [appID, conversationID, varList.length])
const [isCopied, setIsCopied] = React.useState(false)
const handleCopy = useCallback(() => {
copy(currentVar.value)
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, 2000)
}, [currentVar.value])
useMount(() => {
getChatVarLatestValues()
})
return (
<Modal
isShow
onClose={() => { }}
className={cn('w-[920px] max-w-[920px] h-[640px] p-0')}
>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onHide}>
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div>
<div className='w-full h-full flex'>
{/* LEFT */}
<div className='shrink-0 flex flex-col w-[224px] h-full bg-background-sidenav-bg border-r border-divider-burn'>
<div className='shrink-0 pt-5 pl-5 pr-4 pb-3 text-text-primary system-xl-semibold'>{t('workflow.chatVariable.panelTitle')}</div>
<div className='grow overflow-y-auto px-3 py-2'>
{varList.map(chatVar => (
<div key={chatVar.id} className={cn('group mb-0.5 p-2 flex items-center radius-md hover:bg-state-base-hover cursor-pointer', currentVar.id === chatVar.id && 'bg-state-base-hover')} onClick={() => setCurrentVar(chatVar)}>
<BubbleX className={cn('shrink-0 mr-1 w-4 h-4 text-text-tertiary group-hover:text-util-colors-teal-teal-700', currentVar.id === chatVar.id && 'text-util-colors-teal-teal-700')} />
<div title={chatVar.name} className={cn('text-text-tertiary system-sm-medium truncate group-hover:text-util-colors-teal-teal-700', currentVar.id === chatVar.id && 'text-util-colors-teal-teal-700')}>{chatVar.name}</div>
</div>
))}
</div>
</div>
{/* RIGHT */}
<div className='grow flex flex-col h-full bg-components-panel-bg'>
<div className='shrink-0 p-4 pb-2'>
<div className='flex items-center gap-1 py-1'>
<div className='text-text-primary system-xl-semibold'>{currentVar.name}</div>
<div className='text-text-tertiary system-xs-medium'>{capitalize(currentVar.value_type)}</div>
</div>
</div>
<div className='grow p-4 pt-2 flex flex-col'>
<div className='shrink-0 mb-2 flex items-center gap-2'>
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('workflow.chatVariable.storedContent').toLocaleUpperCase()}</div>
<div className='grow h-[1px]' style={{
background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255) 100%)',
}}></div>
{latestValueTimestampMap[currentVar.id] && (
<div className='shrink-0 text-text-tertiary system-xs-regular'>{t('workflow.chatVariable.updatedAt')}{formatTime(latestValueTimestampMap[currentVar.id], t('appLog.dateTimeFormat') as string)}</div>
)}
</div>
<div className='grow'>
{currentVar.value_type !== ChatVarType.Number && currentVar.value_type !== ChatVarType.String && (
<div className='h-full flex flex-col bg-components-input-bg-normal rounded-lg px-2 pb-2'>
<div className='shrink-0 flex justify-between items-center h-7 pt-1 pl-3 pr-2'>
<div className='text-text-secondary system-xs-semibold'>JSON</div>
<div className='flex items-center p-1'>
{!isCopied
? (
<Clipboard className='w-4 h-4 text-text-tertiary cursor-pointer' onClick={handleCopy} />
)
: (
<ClipboardCheck className='w-4 h-4 text-text-tertiary' />
)
}
</div>
</div>
<div className='grow pl-4'>
<CodeEditor
readOnly
noWrapper
isExpand
language={CodeLanguage.json}
value={latestValueMap[currentVar.id] || ''}
isJSONStringifyBeauty
/>
</div>
</div>
)}
{(currentVar.value_type === ChatVarType.Number || currentVar.value_type === ChatVarType.String) && (
<div className='h-full px-4 py-3 rounded-lg bg-components-input-bg-normal text-components-input-text-filled system-md-regular overflow-y-auto'>{latestValueMap[currentVar.id] || ''}</div>
)}
</div>
</div>
</div>
</div>
</Modal>
)
}
export default ConversationVariableModal

View File

@@ -35,7 +35,7 @@ export const useChat = (
const { notify } = useToastContext()
const { handleRun } = useWorkflowRun()
const hasStopResponded = useRef(false)
const connversationId = useRef('')
const conversationID = useRef('')
const taskIdRef = useRef('')
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
@@ -100,7 +100,7 @@ export const useChat = (
}, [handleResponding, stopChat])
const handleRestart = useCallback(() => {
connversationId.current = ''
conversationID.current = ''
taskIdRef.current = ''
handleStop()
const newChatList = config?.opening_statement
@@ -185,7 +185,7 @@ export const useChat = (
handleResponding(true)
const bodyParams = {
conversation_id: connversationId.current,
conversation_id: conversationID.current,
...params,
}
if (bodyParams?.files?.length) {
@@ -214,7 +214,7 @@ export const useChat = (
}
if (isFirstMessage && newConversationId)
connversationId.current = newConversationId
conversationID.current = newConversationId
taskIdRef.current = taskId
if (messageId)
@@ -403,7 +403,7 @@ export const useChat = (
}, [handleRun, handleResponding, handleUpdateChatList, notify, t, updateCurrentQA, config.suggested_questions_after_answer?.enabled])
return {
conversationId: connversationId.current,
conversationId: conversationID.current,
chatList,
handleSend,
handleStop,

View File

@@ -1,19 +1,26 @@
import {
memo,
useRef,
useState,
} from 'react'
import { useKeyPress } from 'ahooks'
import { RiCloseLine } from '@remixicon/react'
import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import {
useEdgesInteractions,
useNodesInteractions,
useWorkflowInteractions,
} from '../../hooks'
import { BlockEnum } from '../../types'
import type { StartNodeType } from '../../nodes/start/types'
import ChatWrapper from './chat-wrapper'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { useStore } from '@/app/components/workflow/store'
export type ChatWrapperRefType = {
handleRestart: () => void
@@ -24,6 +31,13 @@ const DebugAndPreview = () => {
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
const varList = useStore(s => s.conversationVariables)
const [expanded, setExpanded] = useState(true)
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || []
const [showConversationVariableModal, setShowConversationVariableModal] = useState(false)
const handleRestartChat = () => {
handleNodeCancelRunningStatus()
@@ -40,28 +54,43 @@ const DebugAndPreview = () => {
return (
<div
className={cn(
'flex flex-col w-[400px] rounded-l-2xl h-full border border-black/2',
'flex flex-col w-[420px] rounded-l-2xl h-full border border-black/2',
)}
style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',
}}
>
<div className='shrink-0 flex items-center justify-between pl-4 pr-3 pt-3 pb-2 font-semibold text-gray-900'>
{t('workflow.common.debugAndPreview').toLocaleUpperCase()}
<div className='flex items-center'>
<Button
onClick={() => handleRestartChat()}
<div className='shrink-0 flex items-center justify-between px-4 pt-3 pb-2 text-text-primary system-xl-semibold'>
<div className='h-8'>{t('workflow.common.debugAndPreview').toLocaleUpperCase()}</div>
<div className='flex items-center gap-1'>
<TooltipPlus
popupContent={t('common.operation.refresh')}
>
<RefreshCcw01 className='shrink-0 mr-1 w-3 h-3 text-gray-500' />
<div
className='grow truncate uppercase'
title={t('common.operation.refresh') || ''}
<ActionButton onClick={() => handleRestartChat()}>
<RefreshCcw01 className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
{varList.length > 0 && (
<TooltipPlus
popupContent={t('workflow.chatVariable.panelTitle')}
>
{t('common.operation.refresh')}
<ActionButton onClick={() => setShowConversationVariableModal(true)}>
<BubbleX className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
)}
{variables.length > 0 && (
<div className='relative'>
<TooltipPlus
popupContent={t('workflow.panel.userInputField')}
>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
<RiEqualizer2Line className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
{expanded && <div className='absolute z-10 bottom-[-17px] right-[5px] w-3 h-3 bg-components-panel-on-panel-item-bg border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle rotate-45'/>}
</div>
<div className='shrink-0 ml-1 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>Shift</div>
<div className='shrink-0 ml-0.5 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>R</div>
</Button>
)}
<div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
<div
className='flex items-center justify-center w-6 h-6 cursor-pointer'
@@ -72,7 +101,12 @@ const DebugAndPreview = () => {
</div>
</div>
<div className='grow rounded-b-2xl overflow-y-auto'>
<ChatWrapper ref={chatRef} />
<ChatWrapper
ref={chatRef}
showConversationVariableModal={showConversationVariableModal}
onConversationModalHide={() => setShowConversationVariableModal(false)}
showInputsFieldsPanel={expanded}
/>
</div>
</div>
)

View File

@@ -1,10 +1,7 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import { RiArrowDownSLine } from '@remixicon/react'
import FormItem from '../../nodes/_base/components/before-run-form/form-item'
import { BlockEnum } from '../../types'
import {
@@ -12,11 +9,10 @@ import {
useWorkflowStore,
} from '../../store'
import type { StartNodeType } from '../../nodes/start/types'
import cn from '@/utils/classnames'
const UserInput = () => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
const [expanded, setExpanded] = useState(true)
const inputs = useStore(s => s.inputs)
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
@@ -33,46 +29,21 @@ const UserInput = () => {
return null
return (
<div
className={`
relative rounded-xl border z-[1]
${!expanded ? 'bg-indigo-25 border-indigo-100 shadow-none' : 'bg-white shadow-xs border-transparent'}
`}
>
<div
className={`
flex items-center px-2 pt-4 h-[18px] text-[13px] font-semibold cursor-pointer
${!expanded ? 'text-indigo-800' : 'text-gray-800'}
`}
onClick={() => setExpanded(!expanded)}
>
<RiArrowDownSLine
className={`mr-1 w-3 h-3 ${!expanded ? '-rotate-90 text-indigo-600' : 'text-gray-300'}`}
/>
{t('workflow.panel.userInputField').toLocaleUpperCase()}
</div>
<div className='px-2 pt-1 pb-3'>
{
expanded && (
<div className='py-2 text-[13px] text-gray-900'>
{
variables.map((variable, index) => (
<div
key={variable.variable}
className='mb-2 last-of-type:mb-0'
>
<FormItem
autoFocus={index === 0}
payload={variable}
value={inputs[variable.variable]}
onChange={v => handleValueChange(variable.variable, v)}
/>
</div>
))
}
</div>
)
}
<div className={cn('relative bg-components-panel-on-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs z-[1]')}>
<div className='px-4 pt-3 pb-4'>
{variables.map((variable, index) => (
<div
key={variable.variable}
className='mb-4 last-of-type:mb-0'
>
<FormItem
autoFocus={index === 0}
payload={variable}
value={inputs[variable.variable]}
onChange={v => handleValueChange(variable.variable, v)}
/>
</div>
))}
</div>
</div>
)

View File

@@ -0,0 +1,53 @@
import { memo, useState } from 'react'
import { capitalize } from 'lodash-es'
import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { useStore } from '@/app/components/workflow/store'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type EnvItemProps = {
env: EnvironmentVariable
onEdit: (env: EnvironmentVariable) => void
onDelete: (env: EnvironmentVariable) => void
}
const EnvItem = ({
env,
onEdit,
onDelete,
}: EnvItemProps) => {
const envSecrets = useStore(s => s.envSecrets)
const [destructive, setDestructive] = useState(false)
return (
<div className={cn(
'mb-1 px-2.5 py-2 bg-components-panel-on-panel-item-bg radius-md border border-components-panel-border-subtle shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
)}>
<div className='flex items-center justify-between'>
<div className='grow flex gap-1 items-center'>
<Env className='w-4 h-4 text-util-colors-violet-violet-600' />
<div className='text-text-primary system-sm-medium'>{env.name}</div>
<div className='text-text-tertiary system-xs-medium'>{capitalize(env.value_type)}</div>
{env.value_type === 'secret' && <RiLock2Line className='w-3 h-3 text-text-tertiary' />}
</div>
<div className='shrink-0 flex gap-1 items-center text-text-tertiary'>
<div className='p-1 radius-md cursor-pointer hover:bg-state-base-hover hover:text-text-secondary'>
<RiEditLine className='w-4 h-4' onClick={() => onEdit(env)}/>
</div>
<div
className='p-1 radius-md cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive'
onMouseOver={() => setDestructive(true)}
onMouseOut={() => setDestructive(false)}
>
<RiDeleteBinLine className='w-4 h-4' onClick={() => onDelete(env)} />
</div>
</div>
</div>
<div className='text-text-tertiary system-xs-regular truncate'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div>
</div>
)
}
export default memo(EnvItem)

View File

@@ -3,15 +3,14 @@ import {
useCallback,
useState,
} from 'react'
import { capitalize } from 'lodash-es'
import {
useStoreApi,
} from 'reactflow'
import { RiCloseLine, RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useStore } from '@/app/components/workflow/store'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import VariableTrigger from '@/app/components/workflow/panel/env-panel/variable-trigger'
import EnvItem from '@/app/components/workflow/panel/env-panel/env-item'
import type {
EnvironmentVariable,
} from '@/app/components/workflow/types'
@@ -61,6 +60,11 @@ const EnvPanel = () => {
setNodes(newNodes)
}, [getEffectedNodes, store])
const handleEdit = (env: EnvironmentVariable) => {
setCurrentVar(env)
setShowVariableModal(true)
}
const handleDelete = useCallback((env: EnvironmentVariable) => {
removeUsedVarInNodes(env)
updateEnvList(envList.filter(e => e.id !== env.id))
@@ -145,7 +149,7 @@ const EnvPanel = () => {
return (
<div
className={cn(
'relative flex flex-col w-[400px] bg-components-panel-bg-alt rounded-l-2xl h-full border border-components-panel-border',
'relative flex flex-col w-[420px] bg-components-panel-bg-alt rounded-l-2xl h-full border border-components-panel-border',
)}
>
<div className='shrink-0 flex items-center justify-between p-4 pb-0 text-text-primary system-xl-semibold'>
@@ -171,31 +175,12 @@ const EnvPanel = () => {
</div>
<div className='grow px-4 rounded-b-2xl overflow-y-auto'>
{envList.map(env => (
<div
key={env.name}
className='mb-1 px-2.5 py-2 bg-components-panel-on-panel-item-bg radius-md border-[0.5px] border-components-panel-border-subtle shadow-xs'
>
<div className='flex items-center justify-between'>
<div className='grow flex gap-1 items-center'>
<Env className='w-4 h-4 text-util-colors-violet-violet-600' />
<div className='text-text-primary system-sm-medium'>{env.name}</div>
<div className='text-text-tertiary system-xs-medium'>{capitalize(env.value_type)}</div>
{env.value_type === 'secret' && <RiLock2Line className='w-3 h-3 text-text-tertiary' />}
</div>
<div className='shrink-0 flex gap-1 items-center text-text-tertiary'>
<div className='p-1 radius-md cursor-pointer hover:bg-state-base-hover hover:text-text-secondary'>
<RiEditLine className='w-4 h-4' onClick={() => {
setCurrentVar(env)
setShowVariableModal(true)
}}/>
</div>
<div className='p-1 radius-md cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive'>
<RiDeleteBinLine className='w-4 h-4' onClick={() => deleteCheck(env)} />
</div>
</div>
</div>
<div className='text-text-tertiary system-xs-regular truncate'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div>
</div>
<EnvItem
key={env.id}
env={env}
onEdit={handleEdit}
onDelete={deleteCheck}
/>
))}
</div>
<RemoveEffectVarConfirm

View File

@@ -80,7 +80,7 @@ const VariableModal = ({
<div className='px-4 py-2'>
{/* type */}
<div className='mb-4'>
<div className='mb-1 text-text-secondary system-sm-semibold'>{t('workflow.env.modal.type')}</div>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold'>{t('workflow.env.modal.type')}</div>
<div className='flex gap-2'>
<div className={cn(
'w-[106px] flex items-center justify-center p-2 radius-md bg-components-option-card-option-bg border border-components-option-card-option-border text-text-secondary system-sm-regular cursor-pointer hover:shadow-xs hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover',
@@ -111,11 +111,11 @@ const VariableModal = ({
</div>
{/* name */}
<div className='mb-4'>
<div className='mb-1 text-text-secondary system-sm-semibold'>{t('workflow.env.modal.name')}</div>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold'>{t('workflow.env.modal.name')}</div>
<div className='flex'>
<input
tabIndex={0}
className='block px-3 w-full h-9 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
className='block px-3 w-full h-8 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.env.modal.namePlaceholder') || ''}
value={name}
onChange={e => handleNameChange(e.target.value)}
@@ -125,11 +125,11 @@ const VariableModal = ({
</div>
{/* value */}
<div className=''>
<div className='mb-1 text-text-secondary system-sm-semibold'>{t('workflow.env.modal.value')}</div>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold'>{t('workflow.env.modal.value')}</div>
<div className='flex'>
<input
tabIndex={0}
className='block px-3 w-full h-9 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
className='block px-3 w-full h-8 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
value={value}
onChange={e => setValue(e.target.value)}

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import VariableModal from '@/app/components/workflow/panel/env-panel/variable-modal'
// import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,

View File

@@ -13,6 +13,7 @@ import DebugAndPreview from './debug-and-preview'
import Record from './record'
import WorkflowPreview from './workflow-preview'
import ChatRecord from './chat-record'
import ChatVariablePanel from './chat-variable-panel'
import EnvPanel from './env-panel'
import cn from '@/utils/classnames'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -25,6 +26,7 @@ const Panel: FC = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const showEnvPanel = useStore(s => s.showEnvPanel)
const showChatVariablePanel = useStore(s => s.showChatVariablePanel)
const isRestoring = useStore(s => s.isRestoring)
const {
enableShortcuts,
@@ -90,6 +92,11 @@ const Panel: FC = () => {
<EnvPanel />
)
}
{
showChatVariablePanel && (
<ChatVariablePanel />
)
}
</div>
)
}

View File

@@ -11,6 +11,7 @@ import type {
} from './help-line/types'
import type { VariableAssignerNodeType } from './nodes/variable-assigner/types'
import type {
ConversationVariable,
Edge,
EnvironmentVariable,
HistoryWorkflowData,
@@ -21,6 +22,24 @@ import type {
} from './types'
import { WorkflowContext } from './context'
// #TODO chatVar#
// const MOCK_DATA = [
// {
// id: 'fjlaksdjflkjg-dfjlajfl0dnfkafjk-djfdkafj-djfak',
// name: 'chat_history',
// value_type: 'array[message]',
// value: [],
// description: 'The chat history of the conversation',
// },
// {
// id: 'fljdaklfjl-dfjlafj0-dklajglje-eknglh',
// name: 'order_id',
// value: '123456',
// value_type: 'string',
// description: '',
// },
// ]
type PreviewRunningData = WorkflowRunningData & {
resultTabActive?: boolean
resultText?: string
@@ -90,6 +109,10 @@ type Shape = {
setEnvironmentVariables: (environmentVariables: EnvironmentVariable[]) => void
envSecrets: Record<string, string>
setEnvSecrets: (envSecrets: Record<string, string>) => void
showChatVariablePanel: boolean
setShowChatVariablePanel: (showChatVariablePanel: boolean) => void
conversationVariables: ConversationVariable[]
setConversationVariables: (conversationVariables: ConversationVariable[]) => void
selection: null | { x1: number; y1: number; x2: number; y2: number }
setSelection: (selection: Shape['selection']) => void
bundleNodeSize: { width: number; height: number } | null
@@ -204,6 +227,10 @@ export const createWorkflowStore = () => {
setEnvironmentVariables: environmentVariables => set(() => ({ environmentVariables })),
envSecrets: {},
setEnvSecrets: envSecrets => set(() => ({ envSecrets })),
showChatVariablePanel: false,
setShowChatVariablePanel: showChatVariablePanel => set(() => ({ showChatVariablePanel })),
conversationVariables: [],
setConversationVariables: conversationVariables => set(() => ({ conversationVariables })),
selection: null,
setSelection: selection => set(() => ({ selection })),
bundleNodeSize: null,

View File

@@ -8,6 +8,7 @@ import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { NodeTracing } from '@/types/workflow'
import type { Collection, Tool } from '@/app/components/tools/types'
import type { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
export enum BlockEnum {
Start = 'start',
@@ -25,6 +26,7 @@ export enum BlockEnum {
Tool = 'tool',
ParameterExtractor = 'parameter-extractor',
Iteration = 'iteration',
Assigner = 'assigner', // is now named as VariableAssigner
}
export type Branch = {
@@ -109,6 +111,14 @@ export type EnvironmentVariable = {
value_type: 'string' | 'number' | 'secret'
}
export type ConversationVariable = {
id: string
name: string
value_type: ChatVarType
value: any
description: string
}
export type VariableWithValue = {
key: string
value: string
@@ -132,6 +142,7 @@ export type InputVar = {
nodeType: BlockEnum
nodeName: string
variable: string
isChatVar?: boolean
}
variable: string
max_length?: number
@@ -194,6 +205,7 @@ export enum VarType {
boolean = 'boolean',
object = 'object',
array = 'array',
file = 'file',
arrayString = 'array[string]',
arrayNumber = 'array[number]',
arrayObject = 'array[object]',
@@ -209,6 +221,7 @@ export type Var = {
isSelect?: boolean
options?: string[]
required?: boolean
des?: string
}
export type NodeOutPutVar = {

View File

@@ -142,6 +142,12 @@ button:focus-within {
line-height: 12px;
}
.system-2xs-regular {
font-size: 10px;
font-weight: 400;
line-height: 12px;
}
.system-2xs-medium {
font-size: 10px;
font-weight: 500;

View File

@@ -99,6 +99,33 @@ const translation = {
export: 'Export DSL with secret values ',
},
},
chatVariable: {
panelTitle: 'Conversation Variables',
panelDescription: 'Conversation Variables are used to store interactive information that LLM needs to remember, including conversation history, uploaded files, user preferences. They are read-write. ',
docLink: 'Visit our docs to learn more.',
button: 'Add Variable',
modal: {
title: 'Add Conversation Variable',
editTitle: 'Edit Conversation Variable',
name: 'Name',
namePlaceholder: 'Variable name',
type: 'Type',
value: 'Default Value',
valuePlaceholder: 'Default value, leave blank to not set',
description: 'Description',
descriptionPlaceholder: 'Describe the variable',
editInJSON: 'Edit in JSON',
oneByOne: 'Add one by one',
editInForm: 'Edit in Form',
arrayValue: 'Value',
addArrayValue: 'Add Value',
objectKey: 'Key',
objectType: 'Type',
objectValue: 'Default Value',
},
storedContent: 'Stored content',
updatedAt: 'Updated at ',
},
changeHistory: {
title: 'Change History',
placeholder: 'You haven\'t changed anything yet',
@@ -173,6 +200,7 @@ const translation = {
'http-request': 'HTTP Request',
'variable-assigner': 'Variable Aggregator',
'variable-aggregator': 'Variable Aggregator',
'assigner': 'Variable Assigner',
'iteration-start': 'Iteration Start',
'iteration': 'Iteration',
'parameter-extractor': 'Parameter Extractor',
@@ -189,6 +217,7 @@ const translation = {
'template-transform': 'Convert data to string using Jinja template syntax',
'http-request': 'Allow server requests to be sent over the HTTP protocol',
'variable-assigner': 'Aggregate multi-branch variables into a single variable for unified configuration of downstream nodes.',
'assigner': 'TODO',
'variable-aggregator': 'Aggregate multi-branch variables into a single variable for unified configuration of downstream nodes.',
'iteration': 'Perform multiple steps on a list object until all results are outputted.',
'parameter-extractor': 'Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.',
@@ -215,6 +244,7 @@ const translation = {
checklistResolved: 'All issues are resolved',
organizeBlocks: 'Organize blocks',
change: 'Change',
optional: '(optional)',
},
nodes: {
common: {
@@ -406,6 +436,16 @@ const translation = {
},
setAssignVariable: 'Set assign variable',
},
assigner: {
'assignedVariable': 'Assigned Variable',
'writeMode': 'Write Mode',
'over-write': 'Overwrite',
'append': 'Append',
'plus': 'Plus',
'clear': 'Clear',
'setVariable': 'Set Variable',
'variable': 'Variable',
},
tool: {
toAuthorize: 'To authorize',
inputVars: 'Input Variables',

View File

@@ -99,6 +99,33 @@ const translation = {
export: '导出包含 Secret 值的 DSL',
},
},
chatVariable: {
panelTitle: '会话变量',
panelDescription: '会话变量用于存储 LLM 需要的上下文信息,如用户偏好、对话历史等。它是可读写的。',
docLink: '查看文档了解更多。',
button: '添加变量',
modal: {
title: '添加会话变量',
editTitle: '编辑会话变量',
name: '名称',
namePlaceholder: '变量名',
type: '类型',
value: '默认值',
valuePlaceholder: '默认值,可以为空',
description: '描述',
descriptionPlaceholder: '变量的描述',
editInJSON: '在 JSON 中编辑',
oneByOne: '逐个添加',
editInForm: '在表单中编辑',
arrayValue: '值',
addArrayValue: '添加值',
objectKey: '属性',
objectType: '类型',
objectValue: '默认值',
},
storedContent: '存储内容',
updatedAt: '更新时间 ',
},
changeHistory: {
title: '变更历史',
placeholder: '尚未更改任何内容',
@@ -173,6 +200,7 @@ const translation = {
'http-request': 'HTTP 请求',
'variable-assigner': '变量聚合器',
'variable-aggregator': '变量聚合器',
'assigner': '变量赋值',
'iteration-start': '迭代开始',
'iteration': '迭代',
'parameter-extractor': '参数提取器',
@@ -189,6 +217,7 @@ const translation = {
'template-transform': '使用 Jinja 模板语法将数据转换为字符串',
'http-request': '允许通过 HTTP 协议发送服务器请求',
'variable-assigner': '将多路分支的变量聚合为一个变量,以实现下游节点统一配置。',
'assigner': 'TODO',
'variable-aggregator': '将多路分支的变量聚合为一个变量,以实现下游节点统一配置。',
'iteration': '对列表对象执行多次步骤直至输出所有结果。',
'parameter-extractor': '利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。',
@@ -215,6 +244,7 @@ const translation = {
checklistResolved: '所有问题均已解决',
organizeBlocks: '整理节点',
change: '更改',
optional: '(选填)',
},
nodes: {
common: {
@@ -406,6 +436,16 @@ const translation = {
},
setAssignVariable: '设置赋值变量',
},
assigner: {
'assignedVariable': '赋值的变量',
'writeMode': '写入模式',
'over-write': '覆盖',
'append': '追加',
'plus': '加',
'clear': '清空',
'setVariable': '设置变量',
'variable': '变量',
},
tool: {
toAuthorize: '授权',
inputVars: '输入变量',

View File

@@ -3,6 +3,7 @@ import { get, post } from './base'
import type { CommonResponse } from '@/models/common'
import type {
ChatRunHistoryResponse,
ConversationVariableResponse,
FetchWorkflowDraftResponse,
NodesDefaultConfigsResponse,
WorkflowRunHistoryResponse,
@@ -13,7 +14,7 @@ export const fetchWorkflowDraft = (url: string) => {
return get(url, {}, { silent: true }) as Promise<FetchWorkflowDraftResponse>
}
export const syncWorkflowDraft = ({ url, params }: { url: string; params: Pick<FetchWorkflowDraftResponse, 'graph' | 'features' | 'environment_variables'> }) => {
export const syncWorkflowDraft = ({ url, params }: { url: string; params: Pick<FetchWorkflowDraftResponse, 'graph' | 'features' | 'environment_variables' | 'conversation_variables'> }) => {
return post<CommonResponse & { updated_at: number; hash: string }>(url, { body: params }, { silent: true })
}
@@ -58,3 +59,7 @@ export const fetchNodeDefault = (appId: string, blockType: BlockEnum, query = {}
export const updateWorkflowDraftFromDSL = (appId: string, data: string) => {
return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } })
}
export const fetchCurrentValueOfConversationVariable: Fetcher<ConversationVariableResponse, { url: string; params: { conversation_id: string } }> = ({ url, params }) => {
return get<ConversationVariableResponse>(url, { params })
}

View File

@@ -1,6 +1,7 @@
import type { Viewport } from 'reactflow'
import type {
BlockEnum,
ConversationVariable,
Edge,
EnvironmentVariable,
Node,
@@ -58,6 +59,7 @@ export type FetchWorkflowDraftResponse = {
updated_at: number
tool_published: boolean
environment_variables?: EnvironmentVariable[]
conversation_variables?: ConversationVariable[]
}
export type NodeTracingListResponse = {
@@ -240,3 +242,11 @@ export type NodesDefaultConfigsResponse = {
type: string
config: any
}[]
export type ConversationVariableResponse = {
data: (ConversationVariable & { updated_at: number; created_at: number })[]
has_more: boolean
limit: number
total: number
page: number
}