feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon_cross_fill">
|
||||
<path id="Union" d="M1.75753 2.11084C1.56227 2.3061 1.56227 2.62268 1.75753 2.81795L4.93951 5.99993L1.75753 9.18192C1.56227 9.37718 1.56227 9.69376 1.75753 9.88902L2.11108 10.2426C2.30635 10.4378 2.62293 10.4378 2.81819 10.2426L6.00017 7.06059L9.18215 10.2426C9.37741 10.4378 9.694 10.4378 9.88926 10.2426L10.2428 9.88901C10.4381 9.69375 10.4381 9.37717 10.2428 9.18191L7.06084 5.99993L10.2428 2.81795C10.4381 2.62269 10.4381 2.30611 10.2428 2.11085L9.88926 1.75729C9.694 1.56203 9.37741 1.56203 9.18215 1.75729L6.00017 4.93927L2.81819 1.75729C2.62293 1.56202 2.30635 1.56202 2.11108 1.75729L1.75753 2.11084Z" fill="#060709" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 779 B |
@@ -0,0 +1,73 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5257_104690)">
|
||||
<g filter="url(#filter0_d_5257_104690)">
|
||||
<path
|
||||
d="M3.6 2.68c0-1.008 0-1.512.196-1.897a1.8 1.8 0 0 1 .786-.787C4.967-.2 5.472-.2 6.48-.2H16.8l6 7.2v13.92c0 1.008 0 1.512-.196 1.897a1.8 1.8 0 0 1-.787.787c-.384.196-.889.196-1.897.196H6.48c-1.008 0-1.512 0-1.897-.196a1.8 1.8 0 0 1-.786-.787c-.197-.385-.197-.89-.197-1.897V2.68z"
|
||||
fill="#FF54C5" />
|
||||
<path
|
||||
d="M3.66 2.68c0-.505 0-.88.024-1.178.024-.296.072-.51.165-.692a1.74 1.74 0 0 1 .76-.76c.183-.094.397-.142.693-.166.297-.024.672-.024 1.177-.024h10.292l5.968 7.162V20.92c0 .505 0 .88-.024 1.177-.024.297-.072.51-.165.693a1.74 1.74 0 0 1-.76.76c-.183.093-.397.141-.693.165-.297.025-.673.025-1.178.025H6.48c-.505 0-.88 0-1.177-.024-.296-.025-.51-.073-.693-.166a1.74 1.74 0 0 1-.76-.76c-.093-.183-.141-.396-.165-.693a15.907 15.907 0 0 1-.025-1.177V2.68z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path d="M3.6 1.6A1.8 1.8 0 0 1 5.4-.2h11.4l6 7.2v15a1.8 1.8 0 0 1-1.8 1.8H5.4A1.8 1.8 0 0 1 3.6 22V1.6z"
|
||||
fill="url(#paint0_radial_5257_104690)" fill-opacity=".6" />
|
||||
<g filter="url(#filter1_dd_5257_104690)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16.8-.2l6 7.2h-2.16c-1.345 0-2.017 0-2.53-.262A2.4 2.4 0 0 1 17.06 5.69c-.262-.513-.262-1.185-.262-2.53V-.2z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M16.846-.239l-.107-.127v3.529c0 .67 0 1.175.033 1.578.033.404.1.71.236.976.235.463.612.839 1.075 1.075.266.136.571.202.975.235.403.033.909.033 1.579.033h2.29l-.081-.099-6-7.2z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path
|
||||
d="M16.8-.2l6 7.2h-2.16c-1.345 0-2.017 0-2.53-.262A2.4 2.4 0 0 1 17.06 5.69c-.262-.513-.262-1.185-.262-2.53V-.2z"
|
||||
fill="#FF7BD2" />
|
||||
<path d="M16.8-.2l6 7.2h-3.6a2.4 2.4 0 0 1-2.4-2.4V-.2z" fill="url(#paint1_linear_5257_104690)" fill-opacity=".6" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5257_104690" x="-1.543" y="-3.629" width="29.486" height="34.286" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.714" />
|
||||
<feGaussianBlur stdDeviation="2.571" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104690" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5257_104690" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter1_dd_5257_104690" x="9.479" y="-5.331" width="20.776" height="22.051" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2.4" />
|
||||
<feGaussianBlur stdDeviation="3.6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104690" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.2" />
|
||||
<feGaussianBlur stdDeviation="1.2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="effect1_dropShadow_5257_104690" result="effect2_dropShadow_5257_104690" />
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_5257_104690" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5257_104690" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(23.09998 27.59995 -27.8692 23.32533 4.2 .4)">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".357" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".783" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5257_104690" x1="15.299" y1="4.6" x2="25.75" y2="-.606"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".5" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".723" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5257_104690">
|
||||
<path fill="#fff" transform="translate(.9)" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,80 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5257_104663)">
|
||||
<g filter="url(#filter0_d_5257_104663)">
|
||||
<path
|
||||
d="M3.3 2.88c0-1.008 0-1.512.197-1.897a1.8 1.8 0 0 1 .786-.787C4.668 0 5.173 0 6.18 0H16.5l6 7.2v13.92c0 1.008 0 1.512-.196 1.897a1.8 1.8 0 0 1-.786.787c-.385.196-.89.196-1.898.196H6.18c-1.008 0-1.512 0-1.897-.196a1.8 1.8 0 0 1-.786-.787c-.197-.385-.197-.889-.197-1.897V2.88z"
|
||||
fill="#00A8F3" />
|
||||
<path
|
||||
d="M3.36 2.88c0-.505 0-.88.025-1.177.024-.297.072-.51.165-.693a1.74 1.74 0 0 1 .76-.76c.183-.093.397-.141.693-.166C5.3.06 5.675.06 6.18.06h10.292l5.968 7.162V21.12c0 .505 0 .88-.024 1.177-.024.297-.072.51-.165.693a1.741 1.741 0 0 1-.76.76c-.183.093-.397.142-.693.166-.297.024-.672.024-1.178.024H6.18c-.505 0-.88 0-1.177-.024-.296-.024-.51-.073-.693-.166a1.74 1.74 0 0 1-.76-.76c-.093-.183-.141-.396-.165-.693a15.907 15.907 0 0 1-.025-1.177V2.88z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path d="M3.3 1.8A1.8 1.8 0 0 1 5.1 0h11.4l6 7.2v15a1.8 1.8 0 0 1-1.8 1.8H5.1a1.8 1.8 0 0 1-1.8-1.8V1.8z"
|
||||
fill="url(#paint0_radial_5257_104663)" fill-opacity=".6" />
|
||||
<g clip-path="url(#clip1_5257_104663)" fill="#fff">
|
||||
<path
|
||||
d="M14.1 20.7h-1.8a.601.601 0 0 1-.6-.6v-3a.6.6 0 0 1 .6-.6h1.8v.6h-1.8v3h1.8v.6zM10.5 20.7H9.3a.601.601 0 0 1-.6-.6v-3a.6.6 0 0 1 .6-.6h1.2a.6.6 0 0 1 .6.6v3a.6.6 0 0 1-.6.6zm-1.2-3.6v3h1.2v-3H9.3zM6.9 20.7H5.7v-4.2h1.2a1.201 1.201 0 0 1 1.2 1.2v1.8a1.201 1.201 0 0 1-1.2 1.2zm-.6-.6h.6a.6.6 0 0 0 .6-.6v-1.8a.6.6 0 0 0-.6-.6h-.6v3z" />
|
||||
</g>
|
||||
<g filter="url(#filter1_dd_5257_104663)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16.5 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M16.547-.038l-.107-.128v3.529c0 .67 0 1.176.033 1.578.033.404.1.71.236.976.236.463.612.839 1.075 1.075.266.136.571.202.975.235.403.033.909.033 1.579.033h2.291l-.082-.098-6-7.2z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path
|
||||
d="M16.5 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#00A8F3" />
|
||||
<path d="M16.5 0l6 7.2h-3.6a2.4 2.4 0 0 1-2.4-2.4V0z" fill="url(#paint1_linear_5257_104663)" fill-opacity=".6" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5257_104663" x="-1.842" y="-3.429" width="29.486" height="34.286" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.714" />
|
||||
<feGaussianBlur stdDeviation="2.571" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104663" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5257_104663" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter1_dd_5257_104663" x="9.18" y="-5.131" width="20.776" height="22.051" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2.4" />
|
||||
<feGaussianBlur stdDeviation="3.6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104663" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.2" />
|
||||
<feGaussianBlur stdDeviation="1.2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="effect1_dropShadow_5257_104663" result="effect2_dropShadow_5257_104663" />
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_5257_104663" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5257_104663" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(23.09998 27.59995 -27.8692 23.32533 3.9 .6)">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".357" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".783" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5257_104663" x1="15.001" y1="4.8" x2="25.451" y2="-.405"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".5" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".723" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5257_104663">
|
||||
<path fill="#fff" transform="translate(.9)" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_5257_104663">
|
||||
<path fill="#fff" transform="translate(5.1 13.8)" d="M0 0h9.6v9.6H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,80 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5257_104664)">
|
||||
<g filter="url(#filter0_d_5257_104664)">
|
||||
<path
|
||||
d="M3.4 2.88c0-1.008 0-1.512.196-1.897a1.8 1.8 0 0 1 .787-.787C4.768 0 5.273 0 6.28 0H16.6l6 7.2v13.92c0 1.008 0 1.512-.196 1.897a1.8 1.8 0 0 1-.787.787c-.385.196-.889.196-1.897.196H6.28c-1.008 0-1.512 0-1.897-.196a1.8 1.8 0 0 1-.787-.787c-.196-.385-.196-.889-.196-1.897V2.88z"
|
||||
fill="#00C979" />
|
||||
<path
|
||||
d="M3.46 2.88c0-.505 0-.88.024-1.177.025-.297.073-.51.166-.693a1.74 1.74 0 0 1 .76-.76c.183-.093.396-.141.693-.166C5.4.06 5.775.06 6.28.06h10.292l5.968 7.162V21.12c0 .505 0 .88-.024 1.177-.024.297-.072.51-.165.693a1.74 1.74 0 0 1-.76.76c-.184.093-.397.142-.694.166-.297.024-.672.024-1.177.024H6.28c-.505 0-.88 0-1.177-.024-.297-.024-.51-.073-.693-.166a1.74 1.74 0 0 1-.76-.76c-.093-.183-.141-.396-.166-.693a15.907 15.907 0 0 1-.024-1.177V2.88z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path d="M3.4 1.8A1.8 1.8 0 0 1 5.2 0h11.4l6 7.2v15a1.8 1.8 0 0 1-1.8 1.8H5.2a1.8 1.8 0 0 1-1.8-1.8V1.8z"
|
||||
fill="url(#paint0_radial_5257_104664)" fill-opacity=".6" />
|
||||
<g clip-path="url(#clip1_5257_104664)" fill="#fff">
|
||||
<path
|
||||
d="M13.6 20.7h-1.8v-.6h1.8v-1.2h-1.2a.6.6 0 0 1-.6-.6v-1.2a.6.6 0 0 1 .6-.6h1.8v.6h-1.8v1.2h1.2a.6.6 0 0 1 .6.6v1.2a.6.6 0 0 1-.6.6zM9.4 20.1v-3.6h-.6v4.2h2.4v-.6H9.4zM8.2 16.5h-.6L7 18.3l-.6-1.8h-.6l.826 2.1-.826 2.1h.6l.6-1.8.6 1.8h.6l-.826-2.1.826-2.1z" />
|
||||
</g>
|
||||
<g filter="url(#filter1_dd_5257_104664)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16.6 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M16.646-.038l-.106-.128v3.529c0 .67 0 1.176.033 1.578.033.404.1.71.235.976.236.463.613.839 1.075 1.075.267.136.572.202.976.235.403.033.909.033 1.578.033H22.728l-.082-.098-6-7.2z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path
|
||||
d="M16.6 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#00C979" />
|
||||
<path d="M16.6 0l6 7.2H19a2.4 2.4 0 0 1-2.4-2.4V0z" fill="url(#paint1_linear_5257_104664)" fill-opacity=".6" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5257_104664" x="-1.743" y="-3.429" width="29.486" height="34.286" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.714" />
|
||||
<feGaussianBlur stdDeviation="2.571" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104664" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5257_104664" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter1_dd_5257_104664" x="9.28" y="-5.131" width="20.776" height="22.051" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2.4" />
|
||||
<feGaussianBlur stdDeviation="3.6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104664" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.2" />
|
||||
<feGaussianBlur stdDeviation="1.2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="effect1_dropShadow_5257_104664" result="effect2_dropShadow_5257_104664" />
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_5257_104664" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5257_104664" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(23.09998 27.59995 -27.8692 23.32533 4 .6)">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".357" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".783" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5257_104664" x1="15.1" y1="4.8" x2="25.551" y2="-.405"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".5" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".723" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5257_104664">
|
||||
<path fill="#fff" transform="translate(.9)" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_5257_104664">
|
||||
<path fill="#fff" transform="translate(5.2 13.8)" d="M0 0h9.6v9.6H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_arrow_down_fill">
|
||||
<path id="Union" d="M6.00241 7.17466L9.53794 3.63912C9.7332 3.44386 10.0498 3.44386 10.245 3.63912L10.5986 3.99268C10.7939 4.18794 10.7939 4.50452 10.5986 4.69978L6.70951 8.58887C6.31899 8.97939 5.68582 8.97939 5.2953 8.58887L1.40621 4.69978C1.21095 4.50452 1.21095 4.18794 1.40621 3.99268L1.75977 3.63912C1.95503 3.44386 2.27161 3.44386 2.46687 3.63912L6.00241 7.17466Z" fill="currentColor" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_arrow_down_fill">
|
||||
<path id="Union" d="M6.00241 4.82534L9.53794 8.36088C9.7332 8.55614 10.0498 8.55614 10.245 8.36088L10.5986 8.00732C10.7939 7.81206 10.7939 7.49548 10.5986 7.30022L6.70951 3.41113C6.31899 3.02061 5.68582 3.02061 5.2953 3.41113L1.40621 7.30022C1.21095 7.49548 1.21095 7.81206 1.40621 8.00732L1.75977 8.36088C1.95503 8.55614 2.27161 8.55614 2.46687 8.36088L6.00241 4.82534Z" fill="currentColor" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_ban">
|
||||
<path id="Union" d="M0.666992 8.00008C0.666992 12.0502 3.95024 15.3334 8.00033 15.3334C12.0504 15.3334 15.3337 12.0502 15.3337 8.00008C15.3337 3.94999 12.0504 0.666748 8.00033 0.666748C3.95024 0.666748 0.666992 3.94999 0.666992 8.00008ZM11.7453 12.6881C10.7189 13.5091 9.41694 14.0001 8.00033 14.0001C4.68662 14.0001 2.00033 11.3138 2.00033 8.00008C2.00033 6.58346 2.49127 5.28151 3.31229 4.25508L11.7453 12.6881ZM12.6882 11.7453L4.25508 3.31225C5.28154 2.4911 6.58359 2.00008 8.00033 2.00008C11.314 2.00008 14.0003 4.68637 14.0003 8.00008C14.0003 9.41681 13.5093 10.7189 12.6882 11.7453Z" fill="#060709" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 751 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_warning_circle_fill_palette">
|
||||
<path id="Union" d="M15.9997 30.6666C24.0998 30.6666 30.6663 24.1001 30.6663 15.9999C30.6663 7.89974 24.0998 1.33325 15.9997 1.33325C7.8995 1.33325 1.33301 7.89974 1.33301 15.9999C1.33301 24.1001 7.8995 30.6666 15.9997 30.6666Z" fill="#F22435"/>
|
||||
<g id="Union_2">
|
||||
<path d="M16.0003 9.33325C15.2639 9.33325 14.667 9.93021 14.667 10.6666V17.3333C14.667 18.0696 15.2639 18.6666 16.0003 18.6666C16.7367 18.6666 17.3337 18.0696 17.3337 17.3333V10.6666C17.3337 9.93021 16.7367 9.33325 16.0003 9.33325Z" fill="white"/>
|
||||
<path d="M16.0003 19.9999C15.2639 19.9999 14.667 20.5969 14.667 21.3333C14.667 22.0696 15.2639 22.6666 16.0003 22.6666C16.7367 22.6666 17.3337 22.0696 17.3337 21.3333C17.3337 20.5969 16.7367 19.9999 16.0003 19.9999Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 897 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="2" height="12" viewBox="0 0 2 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector 2594" d="M1 0V12" stroke="#1D1C23" stroke-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 177 B |
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { useUpdateEffect } from 'ahooks';
|
||||
import { IconSpin } from '@douyinfe/semi-icons';
|
||||
|
||||
import { useLoadMore } from '../../hooks/shortcut-bar/use-load-more';
|
||||
const TIME_TO_CANCEL_MOUSE_MOVE = 50;
|
||||
|
||||
export interface LoadMoreListData<TData extends object> {
|
||||
list: TData[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export type LoadMoreListProps<TData extends object> = {
|
||||
className?: string;
|
||||
getId: (data: TData) => string;
|
||||
defaultId?: string;
|
||||
itemRender: (data: TData) => React.ReactNode;
|
||||
defaultList?: TData[];
|
||||
listTopSlot?: React.ReactNode;
|
||||
getMoreListService: (
|
||||
currentData: LoadMoreListData<TData> | undefined,
|
||||
) => Promise<LoadMoreListData<TData>>;
|
||||
onSelect?: (data: TData) => void;
|
||||
onActiveId?: (id: string) => void;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'onSelect'>;
|
||||
|
||||
export const LoadMoreList = <TData extends object>(
|
||||
props: LoadMoreListProps<TData>,
|
||||
) => {
|
||||
const {
|
||||
className,
|
||||
onSelect,
|
||||
getId,
|
||||
itemRender,
|
||||
onActiveId,
|
||||
getMoreListService,
|
||||
defaultId,
|
||||
listTopSlot,
|
||||
defaultList,
|
||||
...restProps
|
||||
} = props;
|
||||
const mouseMovingCancelIdRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const mouseMovingRef = useRef(false);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const activeItemRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
scrollIntoView,
|
||||
activeId,
|
||||
focusTo,
|
||||
goNext,
|
||||
goPrev,
|
||||
loadingMore,
|
||||
loading,
|
||||
} = useLoadMore<TData>({
|
||||
getMoreListService,
|
||||
getId: (item: TData) => getId(item),
|
||||
listRef,
|
||||
defaultList,
|
||||
});
|
||||
|
||||
const list = data?.list ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
onActiveId?.(activeId);
|
||||
}, [activeId]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
const defaultItem = list.find(item => getId(item) === defaultId);
|
||||
if (defaultItem) {
|
||||
focusTo(defaultItem);
|
||||
scrollIntoView(defaultItem);
|
||||
onActiveId?.(defaultId || getId(defaultItem));
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={listRef}
|
||||
tabIndex={1}
|
||||
className={className}
|
||||
onMouseLeave={() => {
|
||||
focusTo(null);
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
clearTimeout(mouseMovingCancelIdRef.current);
|
||||
mouseMovingRef.current = true;
|
||||
mouseMovingCancelIdRef.current = setTimeout(() => {
|
||||
mouseMovingRef.current = false;
|
||||
}, TIME_TO_CANCEL_MOUSE_MOVE);
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
goNext();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
goPrev();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
const selectItem = list.find(item => getId(item) === activeId);
|
||||
selectItem && onSelect?.(selectItem);
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{listTopSlot}
|
||||
{list.map(item => (
|
||||
<div
|
||||
key={getId(item)}
|
||||
data-id={getId(item)}
|
||||
ref={getId(item) === activeId ? activeItemRef : null}
|
||||
onClick={() => {
|
||||
onSelect?.(item);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// 鼠标位于滚动条中,会触发该事件,设置仅在移动鼠标过程中进行更新
|
||||
if (mouseMovingRef.current) {
|
||||
focusTo(item);
|
||||
listRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{itemRender(item)}
|
||||
</div>
|
||||
))}
|
||||
{loadingMore || loading ? (
|
||||
<div className="flex justify-center items-center">
|
||||
<IconSpin style={{ color: '#4D53E8' }} spin />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { useIsSendMessageLock } from '@coze-common/chat-area';
|
||||
|
||||
import { type DSL } from '../../types';
|
||||
import { ChatAreaStateContext } from '../../context/chat-area-state/context';
|
||||
import { type DSLContext } from './widgets/types';
|
||||
import { DSLWidgetsMap } from './widgets';
|
||||
|
||||
const getChildrenIds = (item: DSL['elements'][string]): string[] =>
|
||||
item.children ??
|
||||
((item.props?.Columns ?? []) as { children: string[] }[])?.reduce<string[]>(
|
||||
(res, column) => {
|
||||
if (column.children) {
|
||||
res.push(...column.children);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const DSLRender: FC<
|
||||
{
|
||||
elementId: string;
|
||||
} & IShortCutPanelProps
|
||||
> = ({ elementId, ...context }) => {
|
||||
const { dsl } = context;
|
||||
const item = dsl?.elements[elementId];
|
||||
const itemType = item?.type || '';
|
||||
const Component = itemType in DSLWidgetsMap ? DSLWidgetsMap[itemType] : null;
|
||||
const childrenIds = item && getChildrenIds(item);
|
||||
|
||||
if (!Component) {
|
||||
// TODO slardar report
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Component context={context} props={item?.props}>
|
||||
{childrenIds?.map(childrenId => (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<DSLRender key={childrenId} elementId={childrenId} {...context} />
|
||||
</div>
|
||||
))}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export type IShortCutPanelProps = DSLContext;
|
||||
|
||||
export const ShortCutPanel: FC<IShortCutPanelProps> = ({ dsl, ...context }) => {
|
||||
const isSendMessageLock = useIsSendMessageLock();
|
||||
|
||||
return (
|
||||
<ChatAreaStateContext.Provider value={{ isSendMessageLock }}>
|
||||
<DSLRender elementId={dsl.rootID} dsl={dsl} {...context} />
|
||||
</ChatAreaStateContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { type FormApi } from '@coze-arch/bot-semi/Form';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type DSLComponent, type TValue } from '../types';
|
||||
import { findInputElementsWithDefault } from '../../../../utils/dsl-template';
|
||||
|
||||
type FormValue = Record<string, TValue>;
|
||||
export const DSLForm: DSLComponent = ({
|
||||
context: { onChange, onSubmit, dsl },
|
||||
children,
|
||||
}) => {
|
||||
const formRef = useRef<FormApi>();
|
||||
|
||||
/**
|
||||
* text类型组件交互 支持 placeholder 表示默认值
|
||||
* @param formValues
|
||||
*/
|
||||
const onSubmitWrap = (formValues: FormValue) => {
|
||||
if (!onSubmit) {
|
||||
return;
|
||||
}
|
||||
const inputElementsWithDefault = findInputElementsWithDefault(dsl);
|
||||
|
||||
const newValues = Object.entries(formValues).reduce(
|
||||
(prev: Record<string, TValue>, curr) => {
|
||||
const [field, value] = curr;
|
||||
const input = inputElementsWithDefault.find(i => i.id === field);
|
||||
|
||||
if (input && !value) {
|
||||
prev[field] = input.defaultValue;
|
||||
} else {
|
||||
prev[field] = value;
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
inputElementsWithDefault.forEach(input => {
|
||||
const { id, defaultValue } = input;
|
||||
|
||||
if (id && !(id in newValues)) {
|
||||
newValues[id] = defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
onSubmit(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form<FormValue>
|
||||
className="w-full"
|
||||
autoComplete="off"
|
||||
getFormApi={api => (formRef.current = api)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
onChange={formState => onChange?.(formState.values!)}
|
||||
onSubmit={onSubmitWrap}
|
||||
>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DSLFormUpload } from './upload';
|
||||
import { type DSLComponent } from './types';
|
||||
import { DSLFormInput } from './text-input';
|
||||
import { DSLSubmitButton } from './submit-button';
|
||||
import { DSLFormSelect } from './select';
|
||||
import { DSLRoot } from './root';
|
||||
import { DSLPlaceholer } from './placeholder';
|
||||
import { DSLColumnLayout } from './layout';
|
||||
import { DSLForm } from './form';
|
||||
|
||||
// 组件参数是在运行时决定,无法具体做类型约束
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const DSLWidgetsMap: Record<string, DSLComponent<any>> = {
|
||||
'@flowpd/cici-components/Input': DSLFormInput,
|
||||
'@flowpd/cici-components/Select': DSLFormSelect,
|
||||
'@flowpd/cici-components/Upload': DSLFormUpload,
|
||||
'@flowpd/cici-components/Placeholder': DSLPlaceholer,
|
||||
'@flowpd/cici-components/ColumnLayout': DSLColumnLayout,
|
||||
'@flowpd/cici-components/Form': DSLForm,
|
||||
'@flowpd/cici-components/PageContainer': DSLRoot,
|
||||
'@flowpd/cici-components/Button': DSLSubmitButton,
|
||||
} as const;
|
||||
@@ -0,0 +1,31 @@
|
||||
.label {
|
||||
overflow-x: hidden;
|
||||
|
||||
width: fit-content;
|
||||
max-width: calc(100 - 16px);
|
||||
margin-bottom: 0;
|
||||
padding-right: 0;
|
||||
|
||||
:global(.semi-form-field-label-text) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 2px;
|
||||
padding: 2px;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { Form, Tooltip, Typography } from '@coze-arch/bot-semi';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
export const LabelWithDescription: FC<{
|
||||
name: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}> = ({ name, description, required = true }) => (
|
||||
<div className="w-full flex items-center px-2 mb-[2px]">
|
||||
<Form.Label
|
||||
text={
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true }}
|
||||
className={style.text}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
}
|
||||
required={required}
|
||||
className={style.label}
|
||||
/>
|
||||
{!!description && (
|
||||
<Tooltip content={description}>
|
||||
<IconInfo className={style.icon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
// 本期不需要不支持复布局解析
|
||||
export const DSLColumnLayout: FC<PropsWithChildren> = ({ children }) => (
|
||||
<div className="flex items-center justify-between w-full mb-3 gap-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const DSLPlaceholer: FC = () => (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-lg coz-bg-plus text-center text-xs font-medium coz-fg-secondary "
|
||||
style={{ height: 58 }}
|
||||
>
|
||||
{I18n.t('shortcut_modal_components')}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
export const DSLRoot: FC<PropsWithChildren> = ({ children }) => <>{children}</>;
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type SelectProps } from '@coze-arch/bot-semi/Select';
|
||||
import { UIFormSelect } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type DSLFormFieldCommonProps, type DSLComponent } from '../types';
|
||||
import { LabelWithDescription } from '../label-with-desc';
|
||||
|
||||
export const DSLFormSelect: DSLComponent<
|
||||
DSLFormFieldCommonProps & Pick<SelectProps, 'optionList'>
|
||||
> = ({
|
||||
context: { readonly },
|
||||
props: { name, description, defaultValue, ...props },
|
||||
}) => {
|
||||
const required = !defaultValue?.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LabelWithDescription
|
||||
name={name}
|
||||
description={description}
|
||||
required={required}
|
||||
/>
|
||||
<UIFormSelect
|
||||
disabled={readonly}
|
||||
fieldStyle={{ padding: 0 }}
|
||||
className="w-full"
|
||||
field={name}
|
||||
initValue={defaultValue?.value}
|
||||
noLabel
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.button {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconButton, useFormState } from '@coze-arch/bot-semi';
|
||||
import { IconSend } from '@coze-arch/bot-icons';
|
||||
|
||||
import {
|
||||
type DSLContext,
|
||||
type DSLComponent,
|
||||
type DSLFormFieldCommonProps,
|
||||
} from '../types';
|
||||
import { findInputElementById } from '../../../../utils/dsl-template';
|
||||
import { useChatAreaState } from '../../../../context/chat-area-state';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface DSLSubmitButtonProps {
|
||||
formFields?: string[];
|
||||
}
|
||||
|
||||
const useIsSubmitButtonDisable = ({
|
||||
context: { readonly, dsl },
|
||||
props: { formFields = [] },
|
||||
}: {
|
||||
context: DSLContext;
|
||||
props: Pick<DSLSubmitButtonProps, 'formFields'>;
|
||||
}): boolean => {
|
||||
const formState = useFormState();
|
||||
const disabled = formFields.some(field => {
|
||||
const isEmpty = !formState.values[field];
|
||||
const isError = !!formState.errors?.[field];
|
||||
const inputDefaultValue = findInputElementById(dsl, field)?.props
|
||||
?.defaultValue as DSLFormFieldCommonProps['defaultValue'];
|
||||
|
||||
if (inputDefaultValue?.value) {
|
||||
return isError;
|
||||
}
|
||||
|
||||
return isError || isEmpty;
|
||||
});
|
||||
const { isSendMessageLock } = useChatAreaState();
|
||||
|
||||
return readonly || disabled || isSendMessageLock;
|
||||
};
|
||||
|
||||
export const DSLSubmitButton: DSLComponent<DSLSubmitButtonProps> = ({
|
||||
context,
|
||||
props,
|
||||
}) => {
|
||||
const isDisabled = useIsSubmitButtonDisable({ context, props });
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<IconButton
|
||||
theme="borderless"
|
||||
className={styles.button}
|
||||
htmlType="submit"
|
||||
size="small"
|
||||
disabled={isDisabled}
|
||||
icon={<IconSend />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RuleItem } from '@coze-arch/bot-semi/Form';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type DSLFormFieldCommonProps, type DSLComponent } from '../types';
|
||||
import { LabelWithDescription } from '../label-with-desc';
|
||||
|
||||
const parseRules = (rules: RuleItem[]): RuleItem[] =>
|
||||
rules.map(rule => {
|
||||
if (rule.required) {
|
||||
return {
|
||||
...rule,
|
||||
// required 情况下,禁止输入空格
|
||||
validator: (r, v) => !!v?.trim(),
|
||||
};
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
|
||||
export const DSLFormInput: DSLComponent<DSLFormFieldCommonProps> = ({
|
||||
context: { readonly },
|
||||
props: { name, description, rules, defaultValue, ...props },
|
||||
}) => {
|
||||
const required = !defaultValue?.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LabelWithDescription
|
||||
required={required}
|
||||
name={name}
|
||||
description={description}
|
||||
/>
|
||||
<Form.Input
|
||||
disabled={readonly}
|
||||
fieldStyle={{ padding: 0 }}
|
||||
placeholder={defaultValue?.value}
|
||||
className="w-full"
|
||||
field={name}
|
||||
noLabel
|
||||
rules={parseRules(rules)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type FC } from 'react';
|
||||
|
||||
import { type RuleItem } from '@coze-arch/bot-semi/Form';
|
||||
import { type InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type DSL } from '../../../types';
|
||||
|
||||
export interface FileValue {
|
||||
fileInstance?: File;
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type TValue = string | FileValue | undefined;
|
||||
|
||||
export type TCustomUpload = (uploadParams: {
|
||||
file: File;
|
||||
onProgress?: (percent: number) => void;
|
||||
onSuccess?: (url: string, width?: number, height?: number) => void;
|
||||
onError?: (e: { status?: number }) => void;
|
||||
}) => void;
|
||||
|
||||
export interface DSLContext {
|
||||
dsl: DSL;
|
||||
uploadFile?: TCustomUpload;
|
||||
onChange?: (value: Record<string, TValue>) => void; // 需要兼容 file
|
||||
onSubmit?: (value: Record<string, TValue>) => void;
|
||||
readonly?: boolean; // 支持搭建时的预览模式
|
||||
}
|
||||
|
||||
export interface DSLFormFieldCommonProps {
|
||||
name: string;
|
||||
description?: string;
|
||||
rules: RuleItem[];
|
||||
defaultValue?: {
|
||||
type: InputType;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type DSLComponent<TProps = unknown> = FC<
|
||||
PropsWithChildren<{ context: DSLContext; props: TProps }>
|
||||
>;
|
||||
@@ -0,0 +1,85 @@
|
||||
.upload-button {
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
border-style: dashed
|
||||
}
|
||||
|
||||
button.delete-btn {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
|
||||
* {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #4E40E5;
|
||||
}
|
||||
}
|
||||
|
||||
.file-uploading::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: block;
|
||||
|
||||
width: var(--var-percent);
|
||||
min-width: 15%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #e6e8ff;
|
||||
}
|
||||
|
||||
.container.container-error {
|
||||
.upload-button {
|
||||
border-color: #F22435;
|
||||
}
|
||||
|
||||
.input {
|
||||
border-color: #F22435;
|
||||
|
||||
}
|
||||
|
||||
.file {
|
||||
border-color: #F22435;
|
||||
}
|
||||
}
|
||||
|
||||
.retry {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
margin-right: 12px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: #4E40E5;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useState, useRef, useEffect } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { getFileInfo } from '@coze-common/chat-core';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type FileItem } from '@coze-arch/bot-semi/Upload';
|
||||
import {
|
||||
IconButton,
|
||||
Toast,
|
||||
Typography,
|
||||
UIButton,
|
||||
UIInput,
|
||||
Upload,
|
||||
useFieldApi,
|
||||
withField,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconAdd,
|
||||
IconClose,
|
||||
IconCloseNoCycle,
|
||||
IconCopyLink,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
type DSLFormFieldCommonProps,
|
||||
type DSLComponent,
|
||||
type TValue,
|
||||
type TCustomUpload,
|
||||
} from '../types';
|
||||
import { LabelWithDescription } from '../label-with-desc';
|
||||
import { getFileInfoByFileType } from '../../../../utils/file-const';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
const UploadContent: FC<{
|
||||
file: FileItem;
|
||||
disabled?: boolean;
|
||||
inputType: shortcut_command.InputType;
|
||||
onRemove: () => void;
|
||||
onRetry: () => void;
|
||||
}> = ({ file, disabled, inputType, onRemove, onRetry }) => {
|
||||
const isFailed = file.status === 'uploadFail';
|
||||
const isUploading = file.status === 'uploading';
|
||||
const fileType =
|
||||
file.fileInstance && getFileInfo(file.fileInstance)?.fileType;
|
||||
const fileIcon = fileType && getFileInfoByFileType(fileType)?.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
style.file,
|
||||
'flex border border-solid rounded-lg items-center w-full coz-stroke-primary',
|
||||
{
|
||||
[style['file-uploading'] || '']: isUploading,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
// @ts-expect-error ts 无法识别自定义变量
|
||||
'--var-percent': `${file.percent}%`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={fileIcon ?? file.url}
|
||||
className={classnames(
|
||||
'w-6 h-6',
|
||||
fileType === 'image' &&
|
||||
'rounded border border-solid coz-stroke-primary',
|
||||
)}
|
||||
/>
|
||||
<Typography.Text ellipsis className="mx-2 flex-1 text-sm">
|
||||
{file.name}
|
||||
</Typography.Text>
|
||||
{isFailed ? (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
onRetry();
|
||||
}
|
||||
}}
|
||||
className={style.retry}
|
||||
>
|
||||
<IconClose className="coz-fg-hglt-red" />
|
||||
<div>{I18n.t('Retry')}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<IconButton
|
||||
className={classnames('close-btn w-5 h-5', style['delete-btn'])}
|
||||
disabled={disabled}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
theme="borderless"
|
||||
size="small"
|
||||
icon={<IconCloseNoCycle className={style['delete-icon']} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface UploadProps {
|
||||
value?: unknown;
|
||||
name: string;
|
||||
onChange?: (value: TValue) => void;
|
||||
uploadFile?: TCustomUpload;
|
||||
maxSize?: number;
|
||||
accept?: string;
|
||||
disabled?: boolean;
|
||||
validateStatus?: 'error' | 'success';
|
||||
inputType: shortcut_command.InputType;
|
||||
}
|
||||
|
||||
const FileUpload: FC<
|
||||
UploadProps & {
|
||||
toggle: () => void;
|
||||
}
|
||||
> = ({
|
||||
value,
|
||||
name,
|
||||
uploadFile,
|
||||
onChange,
|
||||
inputType,
|
||||
disabled,
|
||||
toggle,
|
||||
...props
|
||||
}) => {
|
||||
const [file, setFile] = useState<FileItem | undefined>();
|
||||
const fieldApi = useFieldApi(name);
|
||||
const uidRef = useRef<string | undefined>(file?.uid);
|
||||
const onUpload = (newFile: FileItem) => {
|
||||
if (newFile.fileInstance) {
|
||||
setFile({
|
||||
...newFile,
|
||||
percent: 0,
|
||||
status: 'uploading',
|
||||
});
|
||||
// 立即清理错误状态
|
||||
fieldApi.setError(true);
|
||||
uidRef.current = newFile?.uid;
|
||||
uploadFile?.({
|
||||
file: newFile.fileInstance,
|
||||
onProgress: percent => {
|
||||
if (uidRef.current !== newFile.uid) {
|
||||
return;
|
||||
}
|
||||
setFile({
|
||||
...newFile,
|
||||
percent,
|
||||
status: 'uploading',
|
||||
});
|
||||
},
|
||||
onSuccess: (url, width = 0, height = 0) => {
|
||||
if (uidRef.current !== newFile.uid) {
|
||||
return;
|
||||
}
|
||||
onChange?.({
|
||||
fileInstance: newFile.fileInstance,
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
setFile({
|
||||
...newFile,
|
||||
response: url,
|
||||
percent: 100,
|
||||
status: 'success',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
if (uidRef.current !== newFile.uid) {
|
||||
return;
|
||||
}
|
||||
// 上传失败,触发错误状态
|
||||
fieldApi.setError(false);
|
||||
setFile({
|
||||
...newFile,
|
||||
status: 'uploadFail',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload
|
||||
action=""
|
||||
className="w-full"
|
||||
draggable
|
||||
limit={1}
|
||||
{...props}
|
||||
disabled={disabled}
|
||||
onAcceptInvalid={() => {
|
||||
Toast.error(I18n.t('shortcut_Illegal_file_format'));
|
||||
}}
|
||||
onSizeError={() => {
|
||||
if (props.maxSize) {
|
||||
Toast.error(
|
||||
I18n.t('file_too_large', {
|
||||
max_size: `${props.maxSize / 1024}MB`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
customRequest={({ onSuccess }) => {
|
||||
// 即使 action="" ,在不传 customRequest 仍然会触发一次向当前 URL 上传文件的请求
|
||||
// 这里传一个 mock customRequest 来阻止 semi 默认的上传行为
|
||||
onSuccess('');
|
||||
}}
|
||||
showUploadList={false}
|
||||
onChange={({ currentFile }) => {
|
||||
// semi 同一个文件会触发多次 onChange,这里只响应首个
|
||||
if (
|
||||
uidRef.current !== currentFile.uid &&
|
||||
(!props.maxSize ||
|
||||
(currentFile.fileInstance?.size &&
|
||||
currentFile.fileInstance.size <= props.maxSize * 1024))
|
||||
) {
|
||||
onUpload(currentFile);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{file ? (
|
||||
<UploadContent
|
||||
file={file}
|
||||
inputType={inputType}
|
||||
onRemove={() => {
|
||||
uidRef.current = undefined;
|
||||
setFile(undefined);
|
||||
onChange?.('');
|
||||
setTimeout(() => {
|
||||
// 删除文件,清理错误状态避免立刻飘红
|
||||
fieldApi.setError(true);
|
||||
});
|
||||
}}
|
||||
onRetry={() => {
|
||||
if (file) {
|
||||
onUpload(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UIButton
|
||||
icon={<IconAdd />}
|
||||
disabled={disabled}
|
||||
className={classnames(style['upload-button'], 'w-full')}
|
||||
>
|
||||
<span className={style['upload-button-text-short']}>
|
||||
{I18n.t('shortcut_component_upload_component_placeholder')}
|
||||
</span>
|
||||
</UIButton>
|
||||
)}
|
||||
</Upload>
|
||||
{!file && (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon={<IconCopyLink />}
|
||||
onClick={toggle}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FileInput: FC<
|
||||
UploadProps & {
|
||||
toggle: () => void;
|
||||
}
|
||||
> = ({ disabled, onChange, toggle }) => (
|
||||
<>
|
||||
<UIInput disabled={disabled} onChange={onChange} className={style.input} />
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon={<IconCloseNoCycle />}
|
||||
onClick={toggle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
// 为了方便控制向外传递的 value
|
||||
const UploadInner = withField((props: UploadProps) => {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const hasError = props.validateStatus === 'error';
|
||||
const fieldApi = useFieldApi(props.name);
|
||||
|
||||
// 避免清空输入导致的飘红
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
props.onChange?.('');
|
||||
// 避免 onchange 触发校验导致立刻飘红
|
||||
setTimeout(() => {
|
||||
fieldApi.setError(true);
|
||||
});
|
||||
});
|
||||
}, [showInput]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center justify-start gap-2',
|
||||
style.container,
|
||||
hasError && style['container-error'],
|
||||
)}
|
||||
>
|
||||
{showInput ? (
|
||||
<FileInput
|
||||
toggle={() => {
|
||||
setShowInput(false);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<FileUpload
|
||||
toggle={() => {
|
||||
setShowInput(true);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const DSLFormUpload: DSLComponent<
|
||||
DSLFormFieldCommonProps & {
|
||||
maxSize?: number;
|
||||
accept?: string;
|
||||
inputType: shortcut_command.InputType;
|
||||
}
|
||||
> = ({
|
||||
context: { uploadFile, readonly },
|
||||
props: { name, description, rules, ...props },
|
||||
}) => (
|
||||
<div>
|
||||
<LabelWithDescription name={name} description={description} />
|
||||
<UploadInner
|
||||
field={name}
|
||||
noLabel
|
||||
name={name}
|
||||
fieldStyle={{ padding: 0 }}
|
||||
uploadFile={uploadFile}
|
||||
disabled={readonly}
|
||||
rules={readonly ? undefined : rules}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const ChatAreaStateContext = createContext<{
|
||||
isSendMessageLock: boolean;
|
||||
}>({ isSendMessageLock: false });
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { ChatAreaStateContext } from './context';
|
||||
|
||||
export const useChatAreaState = () => useContext(ChatAreaStateContext);
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RefObject, useMemo, useState } from 'react';
|
||||
|
||||
import { useInfiniteScroll } from 'ahooks';
|
||||
|
||||
import { useImperativeLayoutEffect } from '../use-imperative-layout-effect';
|
||||
import { type LoadMoreListData } from '../../components/load-more-list';
|
||||
|
||||
export interface LoadMoreHookProps<TData extends object> {
|
||||
getId: (item: TData) => string;
|
||||
listRef: RefObject<HTMLDivElement>;
|
||||
defaultList?: TData[];
|
||||
getMoreListService: (
|
||||
currentData: LoadMoreListData<TData> | undefined,
|
||||
) => Promise<LoadMoreListData<TData>>;
|
||||
}
|
||||
export const useLoadMore = <TData extends object>(
|
||||
props: LoadMoreHookProps<TData>,
|
||||
) => {
|
||||
const { getId, listRef, getMoreListService, defaultList } = props;
|
||||
const [activeId, setActiveId] = useState('');
|
||||
|
||||
const { data, loadingMore, loading, loadMore } = useInfiniteScroll<
|
||||
LoadMoreListData<TData>
|
||||
>(currentData => getMoreListService(currentData), {
|
||||
target: listRef,
|
||||
isNoMore: d => !d?.hasMore,
|
||||
});
|
||||
|
||||
const resultData = useMemo(() => {
|
||||
if (defaultList) {
|
||||
return {
|
||||
list: defaultList.concat(data?.list ?? []),
|
||||
hasMore: !!data?.hasMore,
|
||||
};
|
||||
}
|
||||
return {
|
||||
list: data?.list ?? [],
|
||||
hasMore: !!data?.hasMore,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
const { list } = resultData;
|
||||
|
||||
const focusTo = (toItem: TData | null) => {
|
||||
if (!toItem) {
|
||||
setActiveId('');
|
||||
return;
|
||||
}
|
||||
if (!listRef.current) {
|
||||
return;
|
||||
}
|
||||
const findItem = list.find(item => getId(toItem) === getId(item));
|
||||
if (!findItem) {
|
||||
return;
|
||||
}
|
||||
const itemId = getId(findItem);
|
||||
setActiveId(itemId);
|
||||
};
|
||||
|
||||
const focusFirst = () => {
|
||||
const firstItem = list[0];
|
||||
firstItem && focusTo(firstItem);
|
||||
};
|
||||
|
||||
const scrollToFirst = () => {
|
||||
if (!listRef.current) {
|
||||
return;
|
||||
}
|
||||
listRef.current.scrollTop = 0;
|
||||
};
|
||||
|
||||
const scrollIntoView = useImperativeLayoutEffect((toItem: TData) => {
|
||||
const itemId = getId(toItem);
|
||||
const itemRef = listRef.current?.querySelector(`[data-id="${itemId}"]`);
|
||||
if (!itemRef) {
|
||||
return;
|
||||
}
|
||||
itemRef.scrollIntoView({
|
||||
behavior: 'instant' as ScrollBehavior,
|
||||
block: 'nearest',
|
||||
});
|
||||
});
|
||||
|
||||
const goNext = () => {
|
||||
const curItem = list.find(item => getId(item) === activeId);
|
||||
if (!curItem) {
|
||||
return;
|
||||
}
|
||||
const { item: nextItem, reachLimit } = getNextActiveItem<TData>({
|
||||
getId,
|
||||
list,
|
||||
curItem,
|
||||
});
|
||||
if (reachLimit) {
|
||||
loadMore();
|
||||
}
|
||||
if (!loadingMore) {
|
||||
focusTo(nextItem);
|
||||
scrollIntoView(nextItem);
|
||||
}
|
||||
};
|
||||
|
||||
const goPrev = () => {
|
||||
const curItem = list.find(item => getId(item) === activeId);
|
||||
if (!curItem) {
|
||||
return;
|
||||
}
|
||||
const { item: prevItem, reachLimit } = getPreviousItem<TData>({
|
||||
getId,
|
||||
list,
|
||||
curItem,
|
||||
});
|
||||
if (reachLimit) {
|
||||
loadMore();
|
||||
}
|
||||
focusTo(prevItem);
|
||||
scrollIntoView(prevItem);
|
||||
};
|
||||
|
||||
return {
|
||||
activeId,
|
||||
focusFirst,
|
||||
focusTo,
|
||||
scrollToFirst,
|
||||
scrollIntoView,
|
||||
goNext,
|
||||
goPrev,
|
||||
loadingMore,
|
||||
data: resultData,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
const getTargetItemAndIndex = <TData extends object>({
|
||||
getId,
|
||||
list,
|
||||
target,
|
||||
}: {
|
||||
getId: (item: TData) => string;
|
||||
list: TData[];
|
||||
target: TData;
|
||||
}) => {
|
||||
let targetIndex = -1;
|
||||
const targetItem = list.find((item, index) => {
|
||||
if (getId(item) === getId(target)) {
|
||||
targetIndex = index;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return {
|
||||
targetItem,
|
||||
targetIndex,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNextActiveItem = <TData extends object>({
|
||||
curItem,
|
||||
list,
|
||||
getId,
|
||||
}: {
|
||||
curItem: TData;
|
||||
list: TData[];
|
||||
getId: (item: TData) => string;
|
||||
}): {
|
||||
reachLimit: boolean;
|
||||
item: TData;
|
||||
} => {
|
||||
const { targetIndex } = getTargetItemAndIndex({
|
||||
getId,
|
||||
list,
|
||||
target: curItem,
|
||||
});
|
||||
if (targetIndex < 0) {
|
||||
return {
|
||||
reachLimit: false,
|
||||
item: curItem,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const reachLimit = targetIndex >= list.length - 3;
|
||||
const nextIndex = (targetIndex + 1) % list.length;
|
||||
const item = list.at(nextIndex) || curItem;
|
||||
return {
|
||||
reachLimit,
|
||||
item,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPreviousItem = <TData extends object>({
|
||||
curItem,
|
||||
list,
|
||||
getId,
|
||||
}: {
|
||||
curItem: TData;
|
||||
list: TData[];
|
||||
getId: (item: TData) => string;
|
||||
}): {
|
||||
reachLimit: boolean;
|
||||
item: TData;
|
||||
} => {
|
||||
const { targetIndex } = getTargetItemAndIndex({
|
||||
getId,
|
||||
list,
|
||||
target: curItem,
|
||||
});
|
||||
if (targetIndex < 0) {
|
||||
return {
|
||||
reachLimit: false,
|
||||
item: curItem,
|
||||
};
|
||||
}
|
||||
const reachLimit = targetIndex === 0;
|
||||
const nextIdx = (targetIndex - 1) % list.length;
|
||||
const item = list.at(nextIdx) || curItem;
|
||||
return {
|
||||
reachLimit,
|
||||
item,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep, merge } from 'lodash-es';
|
||||
import websocketManager from '@coze-common/websocket-manager-adapter';
|
||||
import {
|
||||
getFileInfo,
|
||||
type TextAndFileMixMessageProps,
|
||||
} from '@coze-common/chat-core';
|
||||
import {
|
||||
ContentType,
|
||||
type SendMessageOptions,
|
||||
useSendMultimodalMessage,
|
||||
useSendTextMessage,
|
||||
} from '@coze-common/chat-area';
|
||||
import { type PartialRequired } from '@coze-arch/bot-typings/common';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { ToolType } from '@coze-arch/bot-api/playground_api';
|
||||
import type { ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
|
||||
import { getQueryFromTemplate } from '../utils/shortcut-query';
|
||||
import { enableSendTypePanelHideTemplate } from '../shortcut-tool/shortcut-edit/method';
|
||||
import {
|
||||
type OnBeforeSendQueryShortcutParams,
|
||||
type OnBeforeSendTemplateShortcutParams,
|
||||
} from '../shortcut-bar/types';
|
||||
import {
|
||||
type FileValue,
|
||||
type TValue,
|
||||
} from '../components/short-cut-panel/widgets/types';
|
||||
|
||||
export const useSendTextQueryMessage = () => {
|
||||
const sendTextMessage = useSendTextMessage();
|
||||
return (params: {
|
||||
queryTemplate: string;
|
||||
options?: SendMessageOptions;
|
||||
onBeforeSend?: (
|
||||
sendParams: OnBeforeSendQueryShortcutParams,
|
||||
) => OnBeforeSendQueryShortcutParams;
|
||||
shortcut: ShortCutCommand;
|
||||
}) => {
|
||||
const {
|
||||
queryTemplate,
|
||||
onBeforeSend,
|
||||
options: inputOptions,
|
||||
shortcut,
|
||||
} = params;
|
||||
const { tool_type } = shortcut;
|
||||
const useTool =
|
||||
tool_type !== undefined &&
|
||||
[ToolType.ToolTypeWorkFlow, ToolType.ToolTypePlugin].includes(tool_type);
|
||||
|
||||
const message = {
|
||||
payload: {
|
||||
text: queryTemplate,
|
||||
mention_list: [],
|
||||
},
|
||||
};
|
||||
|
||||
const pluginParams = useTool ? getPluginDefaultParams(shortcut) : {};
|
||||
|
||||
const options = merge(
|
||||
{
|
||||
extendFiled: {
|
||||
...pluginParams,
|
||||
device_id: String(websocketManager.deviceId),
|
||||
},
|
||||
},
|
||||
inputOptions,
|
||||
);
|
||||
const { message: newMessage, options: newOptions } = onBeforeSend?.({
|
||||
message,
|
||||
options,
|
||||
}) || {
|
||||
message,
|
||||
options,
|
||||
};
|
||||
sendTextMessage(
|
||||
{
|
||||
text: newMessage.payload.text,
|
||||
mentionList: newMessage.payload.mention_list,
|
||||
},
|
||||
'shortcut',
|
||||
newOptions,
|
||||
);
|
||||
sendTeaEvent(EVENT_NAMES.shortcut_use, {
|
||||
tool_type,
|
||||
use_components: !!shortcut.components_list?.length,
|
||||
show_panel: enableSendTypePanelHideTemplate(shortcut),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const useSendUseToolMessage = () => {
|
||||
const sendMultimodalMessage = useSendMultimodalMessage();
|
||||
return ({
|
||||
shortcut,
|
||||
options: inputOptions,
|
||||
componentsFormValues,
|
||||
onBeforeSendTemplateShortcut,
|
||||
withoutComponentsList = false,
|
||||
}: {
|
||||
shortcut: ShortCutCommand;
|
||||
componentsFormValues: Record<string, TValue>;
|
||||
options?: SendMessageOptions;
|
||||
onBeforeSendTemplateShortcut?: (
|
||||
params: OnBeforeSendTemplateShortcutParams,
|
||||
) => OnBeforeSendTemplateShortcutParams;
|
||||
withoutComponentsList?: boolean;
|
||||
}) => {
|
||||
const { tool_type } = shortcut;
|
||||
const sendQuery = getTemplateQuery(
|
||||
shortcut,
|
||||
componentsFormValues,
|
||||
/**
|
||||
* 无参数调用 store 场景下没有 componentList
|
||||
*/
|
||||
withoutComponentsList,
|
||||
);
|
||||
const useTool =
|
||||
tool_type !== undefined &&
|
||||
[ToolType.ToolTypeWorkFlow, ToolType.ToolTypePlugin].includes(tool_type);
|
||||
|
||||
const pluginParams = useTool
|
||||
? getPluginParams(shortcut, componentsFormValues)
|
||||
: {};
|
||||
|
||||
const imageAndFileList = getImageAndFileList(componentsFormValues);
|
||||
|
||||
const message: TextAndFileMixMessageProps = {
|
||||
payload: {
|
||||
mixList: [
|
||||
{
|
||||
type: ContentType.Text,
|
||||
// TODO 需要看下是否能够优化
|
||||
/**
|
||||
* 防止发送空消息(没有对话的气泡框) => 使用空格占位
|
||||
*/
|
||||
text: sendQuery || ' ',
|
||||
},
|
||||
...imageAndFileList,
|
||||
],
|
||||
mention_list: [],
|
||||
},
|
||||
};
|
||||
|
||||
const options = merge(
|
||||
{
|
||||
extendFiled: {
|
||||
...pluginParams,
|
||||
device_id: String(websocketManager.deviceId),
|
||||
},
|
||||
},
|
||||
inputOptions,
|
||||
);
|
||||
|
||||
const handledParams = onBeforeSendTemplateShortcut?.({
|
||||
message: cloneDeep(message),
|
||||
options: cloneDeep(options),
|
||||
}) || {
|
||||
message,
|
||||
options,
|
||||
};
|
||||
sendMultimodalMessage(
|
||||
handledParams.message ?? message,
|
||||
'shortcut',
|
||||
handledParams.options,
|
||||
);
|
||||
sendTeaEvent(EVENT_NAMES.shortcut_use, {
|
||||
tool_type,
|
||||
use_components: !!shortcut.components_list?.length,
|
||||
show_panel: !enableSendTypePanelHideTemplate(shortcut),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
interface ToolParamValue {
|
||||
value: string;
|
||||
resource_type: 'uri' | '';
|
||||
}
|
||||
|
||||
const getPluginParams = (
|
||||
shortcut: ShortCutCommand,
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
) => {
|
||||
const {
|
||||
plugin_id,
|
||||
plugin_api_name,
|
||||
components_list,
|
||||
tool_info: { tool_params_list } = {},
|
||||
} = shortcut;
|
||||
|
||||
const filterImagesValues = filterComponentFormValues(
|
||||
componentsFormValues,
|
||||
value => {
|
||||
const { fileInstance, url } = value;
|
||||
const resourceType = fileInstance ? 'uri' : '';
|
||||
return {
|
||||
value: url,
|
||||
resource_type: resourceType,
|
||||
};
|
||||
},
|
||||
value => ({
|
||||
value,
|
||||
resource_type: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// key: components_list中的parameter属性 value:values中对应的值 | default_value
|
||||
const runPluginVariables = (tool_params_list ?? []).reduce<
|
||||
Record<string, ToolParamValue>
|
||||
>((acc, cur) => {
|
||||
const { default_value, name, refer_component } = cur;
|
||||
if (!name) {
|
||||
return acc;
|
||||
}
|
||||
if (!refer_component) {
|
||||
acc[name] = {
|
||||
value: default_value ?? '',
|
||||
resource_type: '',
|
||||
};
|
||||
return acc;
|
||||
}
|
||||
const targetComponentName = components_list?.find(
|
||||
com => com.parameter === name,
|
||||
)?.name;
|
||||
const componentValue =
|
||||
targetComponentName && filterImagesValues[targetComponentName];
|
||||
if (componentValue) {
|
||||
acc[name] = componentValue as ToolParamValue;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!Object.keys(runPluginVariables).length) {
|
||||
return {
|
||||
shortcut_cmd_id: shortcut.command_id,
|
||||
toolList: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shortcut_cmd_id: shortcut.command_id,
|
||||
toolList: [
|
||||
{
|
||||
plugin_id,
|
||||
api_name: plugin_api_name ?? '',
|
||||
parameters: runPluginVariables,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const getPluginDefaultParams = (shortcut: ShortCutCommand) => {
|
||||
const {
|
||||
plugin_id,
|
||||
plugin_api_name,
|
||||
tool_info: { tool_params_list } = {},
|
||||
} = shortcut;
|
||||
|
||||
// key: components_list中的parameter属性 value:values中对应的值 | default_value
|
||||
const runPluginVariables = (tool_params_list ?? []).reduce<
|
||||
Record<string, ToolParamValue>
|
||||
>((acc, cur) => {
|
||||
const { default_value, name } = cur;
|
||||
if (!name) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[name] = {
|
||||
value: default_value ?? '',
|
||||
resource_type: '',
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
shortcut_cmd_id: shortcut.command_id,
|
||||
toolList: [
|
||||
{
|
||||
plugin_id,
|
||||
api_name: plugin_api_name ?? '',
|
||||
parameters: runPluginVariables,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const getTemplateQuery = (
|
||||
shortcut: ShortCutCommand,
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
withoutComponentsList = false,
|
||||
) => {
|
||||
const { template_query, components_list } = shortcut;
|
||||
if (!template_query) {
|
||||
throw new Error('template_query is not defined');
|
||||
}
|
||||
// 处理图片文件
|
||||
const componentListValue = getComponentListValue(
|
||||
components_list,
|
||||
componentsFormValues,
|
||||
);
|
||||
|
||||
if (withoutComponentsList) {
|
||||
return getQueryFromTemplate(template_query, componentsFormValues ?? {});
|
||||
}
|
||||
|
||||
return getQueryFromTemplate(template_query, componentListValue);
|
||||
};
|
||||
|
||||
const filterComponentFormValues = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
setImageAndFileValue: (value: FileValue) => unknown,
|
||||
setTextValue: (value: string) => unknown,
|
||||
) =>
|
||||
Object.keys(componentsFormValues).reduce<Record<string, unknown>>(
|
||||
(acc, cur) => {
|
||||
const value = componentsFormValues[cur];
|
||||
// 文件类型
|
||||
if (typeof value === 'object' && value.fileInstance) {
|
||||
acc[cur] = setImageAndFileValue(value);
|
||||
return acc;
|
||||
}
|
||||
// 普通文本类型
|
||||
acc[cur] = setTextValue(value as string);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
export const getImageAndFileList = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
): TextAndFileMixMessageProps['payload']['mixList'] =>
|
||||
Object.keys(componentsFormValues).reduce<
|
||||
TextAndFileMixMessageProps['payload']['mixList']
|
||||
>((acc, cur) => {
|
||||
const value = componentsFormValues[cur];
|
||||
if (isComponentFile(value)) {
|
||||
acc.push({
|
||||
type: ContentType.File,
|
||||
file: value.fileInstance,
|
||||
uri: value.url,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
if (isComponentImage(value)) {
|
||||
acc.push({
|
||||
type: ContentType.Image,
|
||||
file: value.fileInstance,
|
||||
uri: value.url,
|
||||
width: value.width || 0,
|
||||
height: value.height || 0,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const isComponentFile = (
|
||||
value: TValue,
|
||||
): value is PartialRequired<FileValue, 'fileInstance' | 'url'> =>
|
||||
Boolean(
|
||||
typeof value === 'object' &&
|
||||
value.fileInstance &&
|
||||
getFileInfo(value.fileInstance)?.fileType !== 'image',
|
||||
);
|
||||
|
||||
const isComponentImage = (
|
||||
value: TValue,
|
||||
): value is PartialRequired<FileValue, 'fileInstance' | 'url'> =>
|
||||
Boolean(
|
||||
typeof value === 'object' &&
|
||||
value.fileInstance &&
|
||||
getFileInfo(value.fileInstance)?.fileType === 'image',
|
||||
);
|
||||
|
||||
// 获取component_list的value, 带上默认值
|
||||
export const getComponentListValue = (
|
||||
componentsList: ShortCutCommand['components_list'],
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
): Record<string, string> => {
|
||||
const filterValues = filterComponentFormValues(
|
||||
componentsFormValues,
|
||||
value => value?.fileInstance?.name,
|
||||
value => value,
|
||||
);
|
||||
|
||||
// key: components_list中的parameter属性 value:values中对应的值 | default_value
|
||||
return (componentsList ?? []).reduce<Record<string, string>>((acc, cur) => {
|
||||
const { default_value, name, hide } = cur;
|
||||
if (!name) {
|
||||
return acc;
|
||||
}
|
||||
if (hide) {
|
||||
acc[name] = default_value?.value ?? '';
|
||||
return acc;
|
||||
}
|
||||
const componentValue = filterValues[name];
|
||||
if (componentValue) {
|
||||
acc[name] = componentValue as string;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useRef, useLayoutEffect } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- x
|
||||
type Destructor = (() => void) | void;
|
||||
type Fn<ARGS extends unknown[]> = (...args: ARGS) => Destructor;
|
||||
|
||||
export const useImperativeLayoutEffect = <Params extends unknown[]>(
|
||||
effect: Fn<Params>,
|
||||
deps: unknown[] = [],
|
||||
) => {
|
||||
const [effectValue, setEffectValue] = useState(0);
|
||||
const paramRef = useRef<Params>();
|
||||
const effectRef = useRef<Fn<Params>>(() => undefined);
|
||||
effectRef.current = effect;
|
||||
useLayoutEffect(() => {
|
||||
if (!effectValue) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- 体操不动, 凑活用吧
|
||||
// @ts-expect-error
|
||||
const params = paramRef.current || ([] as Params);
|
||||
return effectRef.current(...params);
|
||||
}, [effectValue, ...deps]);
|
||||
return (...args: Params) => {
|
||||
paramRef.current = args;
|
||||
setEffectValue(pre => pre + 1);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getFileInfo,
|
||||
type UploadPluginConstructor,
|
||||
} from '@coze-common/chat-core';
|
||||
import { useGetRegisteredPlugin } from '@coze-common/chat-area';
|
||||
|
||||
// 延迟1.5s后开始模拟上传进度
|
||||
const FAKE_PROGRESS_START_DELAY = 1500;
|
||||
// fake progress 初始进度
|
||||
const FAKE_PROGRESS_START = 50;
|
||||
// 最大进度
|
||||
const FAKE_PROGRESS_MAX = 85;
|
||||
// 每次步进值
|
||||
const FAKE_PROGRESS_STEP = 5;
|
||||
// 循环间隔
|
||||
const FAKE_PROGRESS_INTERVAL = 100;
|
||||
|
||||
export const useGetUploadPluginInstance = () => {
|
||||
const getRegisteredPlugin = useGetRegisteredPlugin();
|
||||
|
||||
return ({
|
||||
file,
|
||||
onProgress,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
file: File;
|
||||
onProgress?: (percent: number) => void;
|
||||
onError?: (error: { status: number | undefined }) => void;
|
||||
onSuccess?: (url: string, width: number, height: number) => void;
|
||||
}) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const UploadPlugin: UploadPluginConstructor | null | undefined =
|
||||
getRegisteredPlugin('upload-plugin');
|
||||
if (!UploadPlugin) {
|
||||
return;
|
||||
}
|
||||
const uploader = new UploadPlugin({
|
||||
file,
|
||||
type: getFileInfo(file)?.fileType === 'image' ? 'image' : 'object',
|
||||
});
|
||||
// 如果1s内上传进度没有变化,主动触发fake progress, 500ms内从50%上升到80%,忽略后续的真实进度
|
||||
let isStartFakeProgress = false;
|
||||
let fakeProgressTimer: number | undefined;
|
||||
let fakeProgress = FAKE_PROGRESS_START;
|
||||
const fakeProgressHandler = () => {
|
||||
if (fakeProgress < FAKE_PROGRESS_MAX) {
|
||||
fakeProgress += FAKE_PROGRESS_STEP;
|
||||
onProgress?.(fakeProgress);
|
||||
}
|
||||
};
|
||||
|
||||
const startFakeProgressTimer = setTimeout(() => {
|
||||
isStartFakeProgress = true;
|
||||
fakeProgressTimer = window.setInterval(
|
||||
fakeProgressHandler,
|
||||
FAKE_PROGRESS_INTERVAL,
|
||||
);
|
||||
}, FAKE_PROGRESS_START_DELAY);
|
||||
|
||||
function clearFakeProgress() {
|
||||
clearTimeout(startFakeProgressTimer);
|
||||
clearInterval(fakeProgressTimer);
|
||||
fakeProgressTimer = undefined;
|
||||
fakeProgressTimer = undefined;
|
||||
isStartFakeProgress = false;
|
||||
}
|
||||
uploader.on('progress', ({ percent }) => {
|
||||
// 有假进度,忽略后续的真实进度
|
||||
if (isStartFakeProgress) {
|
||||
return;
|
||||
}
|
||||
startFakeProgressTimer && clearFakeProgress();
|
||||
onProgress?.(percent);
|
||||
});
|
||||
uploader.on('error', e => {
|
||||
onError?.({ status: e.extra.errorCode });
|
||||
clearFakeProgress();
|
||||
});
|
||||
|
||||
uploader.on(
|
||||
'complete',
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
({ uploadResult: { Url, Uri, ImageHeight = 0, ImageWidth = 0 } }) => {
|
||||
{
|
||||
onSuccess?.(Url || Uri || '', ImageWidth, ImageHeight);
|
||||
clearFakeProgress();
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// !Notice 禁止直接导出 shortcut-tool,会导致下游依赖不需要的 knowledge-upload
|
||||
// export { ShortcutToolConfig } from './shortcut-tool';
|
||||
export { ShortcutBar } from './shortcut-bar';
|
||||
|
||||
export { ComponentsTable } from './shortcut-tool/shortcut-edit/components-table';
|
||||
|
||||
export {
|
||||
ShortCutCommand,
|
||||
getStrictShortcuts,
|
||||
} from '@coze-agent-ide/tool-config';
|
||||
|
||||
export type {
|
||||
OnBeforeSendTemplateShortcutParams,
|
||||
OnBeforeSendQueryShortcutParams,
|
||||
} from './shortcut-bar/types';
|
||||
|
||||
export { getUIModeByBizScene } from './utils/get-ui-mode-by-biz-scene';
|
||||
@@ -0,0 +1,13 @@
|
||||
.shortcut-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 快捷指令操作bar
|
||||
import { type CSSProperties, type FC, useRef, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { useMessageWidth } from '@coze-common/chat-area';
|
||||
import { OverflowList, Popover } from '@coze-arch/bot-semi';
|
||||
import { SendType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
enableSendTypePanelHideTemplate,
|
||||
getFormValueFromShortcut,
|
||||
} from '../shortcut-tool/shortcut-edit/method';
|
||||
import { ShortcutTemplate } from '../shortcut-template';
|
||||
import { ShortcutsLoadMoreList } from '../shortcut/load-more/shortcuts-load-more-list';
|
||||
import { TemplateShortcut, LoadMore, QueryShortcut } from '../shortcut';
|
||||
import { useSendUseToolMessage } from '../hooks/shortcut';
|
||||
import { type TValue } from '../components/short-cut-panel/widgets/types';
|
||||
import {
|
||||
type OnBeforeSendQueryShortcutParams,
|
||||
type OnBeforeSendTemplateShortcutParams,
|
||||
type UIMode,
|
||||
} from './types';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
interface ChatShortCutBarProps {
|
||||
shortcuts: ShortCutCommand[];
|
||||
onActiveShortcutChange?: (
|
||||
shortcutInfo?: ShortCutCommand,
|
||||
isTemplateShortcutActive?: boolean,
|
||||
) => void;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
uiMode?: UIMode; // 默认为白色,有背景的时候为模糊
|
||||
defaultId?: string;
|
||||
wrapperStyle?: CSSProperties;
|
||||
toolTipFooterSlot?: React.ReactNode;
|
||||
onBeforeSendTemplateShortcut?: (
|
||||
params: OnBeforeSendTemplateShortcutParams,
|
||||
) => OnBeforeSendTemplateShortcutParams;
|
||||
onBeforeSendTextMessage?: (
|
||||
params: OnBeforeSendQueryShortcutParams,
|
||||
) => OnBeforeSendQueryShortcutParams;
|
||||
popoverTipShowBotInfo?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const ShortcutBar: FC<ChatShortCutBarProps> = props => {
|
||||
const {
|
||||
shortcuts,
|
||||
onActiveShortcutChange,
|
||||
className,
|
||||
wrapperClassName,
|
||||
defaultId,
|
||||
uiMode = 'white',
|
||||
wrapperStyle,
|
||||
toolTipFooterSlot,
|
||||
onBeforeSendTemplateShortcut,
|
||||
onBeforeSendTextMessage,
|
||||
popoverTipShowBotInfo = false,
|
||||
} = props;
|
||||
const overflowListRef = useRef<HTMLDivElement>(null);
|
||||
const [isShowLoadMoreList, setIsShowLoadMoreList] = useState(false);
|
||||
const [activeShortcut, setActiveShortcut] = useState<
|
||||
ShortCutCommand | undefined
|
||||
>(undefined);
|
||||
const [shortcutTemplateVisible, setShortcutTemplateVisible] = useState(false);
|
||||
const sendUseToolMessage = useSendUseToolMessage();
|
||||
const messageWidth = useMessageWidth();
|
||||
|
||||
const handleActiveShortcutChange = (
|
||||
shortcut: ShortCutCommand | undefined,
|
||||
hideTemplate = false,
|
||||
) => {
|
||||
setActiveShortcut(shortcut);
|
||||
|
||||
const isTemplateShortcutActive =
|
||||
shortcut?.send_type === SendType.SendTypePanel && !hideTemplate;
|
||||
|
||||
onActiveShortcutChange?.(shortcut, isTemplateShortcutActive);
|
||||
setShortcutTemplateVisible(isTemplateShortcutActive);
|
||||
};
|
||||
|
||||
const shortcutClick = (shortcut: ShortCutCommand) => {
|
||||
/**
|
||||
* send_type=SendTypePanel 且 components_list hide均为true
|
||||
* 直接发送
|
||||
*/
|
||||
const hideTemplate = enableSendTypePanelHideTemplate(shortcut);
|
||||
|
||||
if (hideTemplate) {
|
||||
onShortcutTemplateNoParamsSubmit(
|
||||
getFormValueFromShortcut(shortcut),
|
||||
shortcut,
|
||||
);
|
||||
}
|
||||
|
||||
handleActiveShortcutChange(shortcut, hideTemplate);
|
||||
|
||||
setIsShowLoadMoreList(false);
|
||||
};
|
||||
|
||||
const closeShortcutTemplate = () => {
|
||||
setShortcutTemplateVisible(false);
|
||||
handleActiveShortcutChange(undefined);
|
||||
};
|
||||
|
||||
const renderShortcut = (shortcut: ShortCutCommand) => (
|
||||
<>
|
||||
{shortcut.send_type === SendType.SendTypeQuery && (
|
||||
<QueryShortcut
|
||||
uiMode={uiMode}
|
||||
key={shortcut.command_id}
|
||||
shortcut={shortcut}
|
||||
onBeforeSend={onBeforeSendTextMessage}
|
||||
toolTipFooterSlot={toolTipFooterSlot}
|
||||
popoverTipShowBotInfo={popoverTipShowBotInfo}
|
||||
onClick={() => shortcutClick(shortcut)}
|
||||
/>
|
||||
)}
|
||||
{shortcut.send_type === SendType.SendTypePanel && (
|
||||
<TemplateShortcut
|
||||
uiMode={uiMode}
|
||||
key={shortcut.command_id}
|
||||
shortcut={shortcut}
|
||||
toolTipFooterSlot={toolTipFooterSlot}
|
||||
popoverTipShowBotInfo={popoverTipShowBotInfo}
|
||||
onClick={() => shortcutClick(shortcut)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const onShortcutTemplateSubmit = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
) => {
|
||||
if (!activeShortcut) {
|
||||
return;
|
||||
}
|
||||
const { agent_id, object_id } = activeShortcut;
|
||||
sendUseToolMessage({
|
||||
shortcut: activeShortcut,
|
||||
options: {
|
||||
extendFiled: {
|
||||
extra: {
|
||||
bot_state: JSON.stringify({
|
||||
agent_id,
|
||||
bot_id: object_id,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
componentsFormValues,
|
||||
onBeforeSendTemplateShortcut,
|
||||
});
|
||||
closeShortcutTemplate();
|
||||
};
|
||||
|
||||
/**
|
||||
* sendType=panel 支持不展示组件直接发送
|
||||
*/
|
||||
const onShortcutTemplateNoParamsSubmit = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
shortcut?: ShortCutCommand,
|
||||
) => {
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { agent_id, object_id, components_list, tool_info } = shortcut;
|
||||
/**
|
||||
* sendType=panel,useTool=true 无参数直接发送
|
||||
*/
|
||||
const withoutComponentsList =
|
||||
!!tool_info?.tool_name && !components_list?.length;
|
||||
|
||||
sendUseToolMessage({
|
||||
shortcut,
|
||||
options: {
|
||||
extendFiled: {
|
||||
extra: {
|
||||
bot_state: JSON.stringify({
|
||||
agent_id,
|
||||
bot_id: object_id,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
componentsFormValues,
|
||||
onBeforeSendTemplateShortcut,
|
||||
withoutComponentsList,
|
||||
});
|
||||
closeShortcutTemplate();
|
||||
};
|
||||
|
||||
if (!shortcuts?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shortcutTemplateVisible && activeShortcut) {
|
||||
return (
|
||||
<ShortcutTemplate
|
||||
shortcut={activeShortcut}
|
||||
onSubmit={onShortcutTemplateSubmit}
|
||||
visible={shortcutTemplateVisible}
|
||||
onClose={() => {
|
||||
handleActiveShortcutChange(undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
style['shortcut-bar'],
|
||||
className,
|
||||
'flex justify-center items-center w-full',
|
||||
)}
|
||||
>
|
||||
<Popover
|
||||
content={
|
||||
<ShortcutsLoadMoreList
|
||||
defaultId={defaultId}
|
||||
shortcuts={shortcuts}
|
||||
onBeforeSendTextMessage={onBeforeSendTextMessage}
|
||||
onSelect={shortcutClick}
|
||||
/>
|
||||
}
|
||||
onVisibleChange={setIsShowLoadMoreList}
|
||||
position={'topLeft'}
|
||||
trigger="custom"
|
||||
visible={isShowLoadMoreList}
|
||||
spacing={{
|
||||
x: 0,
|
||||
y: 9,
|
||||
}}
|
||||
getPopupContainer={() => overflowListRef.current || document.body}
|
||||
onClickOutSide={() => setIsShowLoadMoreList(false)}
|
||||
onEscKeyDown={() => setIsShowLoadMoreList(false)}
|
||||
>
|
||||
<div
|
||||
ref={overflowListRef}
|
||||
className={cls(wrapperClassName, 'relative flex justify-start pb-4')}
|
||||
style={{
|
||||
maxWidth: messageWidth,
|
||||
...wrapperStyle,
|
||||
}}
|
||||
>
|
||||
<OverflowList
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
minVisibleItems={1}
|
||||
items={shortcuts}
|
||||
// @ts-expect-error visibleItemRenderer 有问题
|
||||
visibleItemRenderer={renderShortcut}
|
||||
overflowRenderer={overflowItems => {
|
||||
if (!overflowItems.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<LoadMore
|
||||
uiMode={uiMode}
|
||||
isLoadMoreActive={isShowLoadMoreList}
|
||||
shortcuts={shortcuts}
|
||||
onOpen={() => setIsShowLoadMoreList(true)}
|
||||
onClose={() => setIsShowLoadMoreList(false)}
|
||||
getPopupContainer={() => overflowListRef.current}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import {
|
||||
type SendMessageOptions,
|
||||
type TextAndFileMixMessageProps,
|
||||
type TextMessageProps,
|
||||
} from '@coze-common/chat-core';
|
||||
|
||||
export interface ChatShortCutBarProps {
|
||||
shortcuts: ShortCutCommand[]; // 目前支持两种快捷键
|
||||
onClickShortCut: (shortcutInfo: ShortCutCommand) => void;
|
||||
}
|
||||
// 更新后 home 为 white 调试区、商店为 grey
|
||||
export type UIMode = 'grey' | 'white' | 'blur'; // 默认为白色,有背景的时候为模糊
|
||||
|
||||
export interface OnBeforeSendTemplateShortcutParams {
|
||||
message: TextAndFileMixMessageProps;
|
||||
options?: SendMessageOptions;
|
||||
}
|
||||
|
||||
export interface OnBeforeSendQueryShortcutParams {
|
||||
message: TextMessageProps;
|
||||
options?: SendMessageOptions;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.shortcut-template {
|
||||
margin: 0 24px 36px;
|
||||
background: #FFF;
|
||||
border: 1px solid #4E40E5;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 快捷指令换起的模板组件
|
||||
import { type FC, useMemo, useRef } from 'react';
|
||||
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { useMessageWidth } from '@coze-common/chat-area';
|
||||
import { UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconShortcutTemplateClose } from '@coze-arch/bot-icons';
|
||||
|
||||
import { getDSLFromComponents } from '../utils/dsl-template';
|
||||
import { useGetUploadPluginInstance } from '../hooks/use-upload-plugin';
|
||||
import { type TValue } from '../components/short-cut-panel/widgets/types';
|
||||
import { ShortCutPanel } from '../components/short-cut-panel';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
interface ShortcutTemplateProps {
|
||||
shortcut: Partial<ShortCutCommand>;
|
||||
visible?: boolean;
|
||||
readonly?: boolean;
|
||||
onClose?: () => void;
|
||||
onSubmit?: (componentsFormValues: Record<string, TValue>) => void;
|
||||
}
|
||||
export const ShortcutTemplate: FC<ShortcutTemplateProps> = props => {
|
||||
const { shortcut, onClose, visible, readonly, onSubmit } = props;
|
||||
const shortcutTemplateRef = useRef<HTMLDivElement>(null);
|
||||
const getRegisteredPluginInstance = useGetUploadPluginInstance();
|
||||
const messageWidth = useMessageWidth();
|
||||
|
||||
const dsl = useMemo(() => {
|
||||
const showComponents =
|
||||
shortcut.components_list?.filter(com => !com.hide) ?? [];
|
||||
return getDSLFromComponents(showComponents);
|
||||
}, [shortcut.components_list]);
|
||||
|
||||
const onShortcutPanelSubmit = (values: Record<string, TValue>) => {
|
||||
onSubmit?.(values);
|
||||
};
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={shortcutTemplateRef}
|
||||
className={style['shortcut-template']}
|
||||
style={{
|
||||
width: `calc(${messageWidth} - 48px)`,
|
||||
}}
|
||||
>
|
||||
{/*header*/}
|
||||
<div className="flex items-center text-sm coz-fg-primary px-4 py-[6px] coz-bg-primary rounded-t-3xl h-8">
|
||||
{shortcut.shortcut_icon?.url ? (
|
||||
<img
|
||||
src={shortcut.shortcut_icon.url}
|
||||
alt="icon"
|
||||
className="mr-1 h-[14px]"
|
||||
/>
|
||||
) : null}
|
||||
<div>{shortcut.command_name}</div>
|
||||
<UIIconButton
|
||||
icon={<IconShortcutTemplateClose />}
|
||||
onClick={onClose}
|
||||
wrapperClass="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
{/*content*/}
|
||||
<div className="p-3">
|
||||
<ShortCutPanel
|
||||
uploadFile={({ file, onError, onProgress, onSuccess }) => {
|
||||
getRegisteredPluginInstance?.({
|
||||
file,
|
||||
onProgress,
|
||||
onError,
|
||||
onSuccess,
|
||||
});
|
||||
}}
|
||||
readonly={readonly}
|
||||
onSubmit={onShortcutPanelSubmit}
|
||||
dsl={dsl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 快捷指令在IDE中的配置tool
|
||||
export { ShortcutToolConfig } from './shortcut-config';
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Image } from '@coze-arch/bot-semi';
|
||||
|
||||
import style from '../index.module.less';
|
||||
import shortcutTipEn from '../../../assets/shortcut-tip_en.png';
|
||||
import shortcutTipCn from '../../../assets/shortcut-tip_cn.png';
|
||||
|
||||
export const ShortcutTips = () => (
|
||||
<div className={style['tip-content']}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
{I18n.t('bot_ide_shortcut_intro')}
|
||||
</div>
|
||||
<Image
|
||||
preview={false}
|
||||
width={416}
|
||||
src={IS_OVERSEA ? shortcutTipEn : shortcutTipCn}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,197 @@
|
||||
@ide-tool-prefix: chat-studio-tool-content-block;
|
||||
|
||||
.shortcut-tool-config {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
|
||||
:global {
|
||||
.@{ide-tool-prefix}-content {
|
||||
/* stylelint-disable declaration-no-important */
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
place-content: center space-between;
|
||||
|
||||
height: 52px;
|
||||
margin-bottom: 4px;
|
||||
padding: 8px;
|
||||
|
||||
background: rgba(6, 7, 9, 4%);
|
||||
border-radius: 8px;
|
||||
|
||||
&.shortcut-item-mouse_hover {
|
||||
background: rgba(6, 7, 9, 12%);
|
||||
border: 1px solid rgba(6, 7, 9, 10%);
|
||||
}
|
||||
|
||||
&.shortcut-item_hovered {
|
||||
background: rgba(6, 7, 9, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-item_title {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.shortcut-item_header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: calc(100% - 80px);
|
||||
}
|
||||
|
||||
.operation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.operation-item-icon {
|
||||
cursor: pointer;
|
||||
|
||||
:global {
|
||||
.semi-icon {
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-item-icon_drag {
|
||||
cursor: grab;
|
||||
background: unset !important;
|
||||
|
||||
&.operation-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-item-icon_hover {
|
||||
&:hover {
|
||||
background: rgba(6, 7, 9, 16%);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-modal {
|
||||
:global {
|
||||
.semi-modal {
|
||||
border-radius: 8px;
|
||||
|
||||
.semi-modal-content {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
.semi-modal-header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.semi-modal-footer {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-common-modal-button-style {
|
||||
min-width: 56px;
|
||||
height: 32px;
|
||||
padding: 6px 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
|
||||
background: rgba(6, 7, 9, 8%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delete-modal-cancel-button {
|
||||
.delete-common-modal-button-style;
|
||||
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
}
|
||||
|
||||
.delete-modal-ok-button {
|
||||
.delete-common-modal-button-style;
|
||||
|
||||
color: #fff;
|
||||
background-color: #f22435;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #ba0010 !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #b0000f !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button-16 {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-button {
|
||||
&.semi-button-size-small {
|
||||
height: 16px;
|
||||
padding: 1px !important;
|
||||
|
||||
svg {
|
||||
@apply text-foreground-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tip-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 416px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.shortcut-config-empty {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 快捷指令在IDE中的配置tool
|
||||
import React, { type FC, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useDebounceFn } from 'ahooks';
|
||||
import {
|
||||
type ShortCutStruct,
|
||||
getStrictShortcuts,
|
||||
type ShortCutCommand,
|
||||
ToolKey,
|
||||
} from '@coze-agent-ide/tool-config';
|
||||
import type { IToggleContentBlockEventParams } from '@coze-agent-ide/tool';
|
||||
import {
|
||||
AddButton,
|
||||
EventCenterEventName,
|
||||
ToolContentBlock,
|
||||
useEvent,
|
||||
useToolContentBlockDefaultExpand,
|
||||
useToolDispatch,
|
||||
useToolValidData,
|
||||
} from '@coze-agent-ide/tool';
|
||||
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import {
|
||||
getBotDetailIsReadonly,
|
||||
updateShortcutSort,
|
||||
} from '@coze-studio/bot-detail-store';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { Toast } from '@coze-arch/bot-semi';
|
||||
import { BotMode } from '@coze-arch/bot-api/playground_api';
|
||||
import { PlaygroundApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type SkillsModalProps } from '../types';
|
||||
import { useShortcutEditModal } from '../shortcut-edit';
|
||||
import { isApiError } from '../../utils/handle-error';
|
||||
import { EmptyShortcuts } from './shortcut-list/empty-shortcuts';
|
||||
import { ShortcutList } from './shortcut-list';
|
||||
import { ShortcutTips } from './config-action';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
const MAX_SHORTCUTS = 10;
|
||||
|
||||
export interface ShortcutToolConfigProps {
|
||||
title: string;
|
||||
toolKey: 'shortcut';
|
||||
skillModal: FC<SkillsModalProps>;
|
||||
botMode: BotMode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const ShortcutToolConfig: FC<ShortcutToolConfigProps> = props => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState('');
|
||||
const { title, skillModal: SkillModal, botMode } = props;
|
||||
const { isReadonly, botId } = useBotInfoStore(
|
||||
useShallow(state => ({
|
||||
isReadonly: getBotDetailIsReadonly(),
|
||||
botId: state.botId,
|
||||
})),
|
||||
);
|
||||
const { shortcuts: initShortcuts = [] } = useBotSkillStore(
|
||||
useShallow(state => ({
|
||||
shortcuts: state.shortcut.shortcut_list,
|
||||
})),
|
||||
);
|
||||
|
||||
const getSpaceId = useSpaceStore(state => state.getSpaceId);
|
||||
const setHasValidData = useToolValidData();
|
||||
|
||||
// single不展示指定agent的快捷指令
|
||||
const singleShortcuts = initShortcuts?.filter(shortcut => !shortcut.agent_id);
|
||||
const shortcuts =
|
||||
botMode === BotMode.SingleMode ? singleShortcuts : initShortcuts;
|
||||
|
||||
const hasConfiguredShortcuts = Boolean(shortcuts && shortcuts.length > 0);
|
||||
setHasValidData(hasConfiguredShortcuts);
|
||||
|
||||
const isReachLimit = shortcuts.length >= MAX_SHORTCUTS;
|
||||
|
||||
const defaultExpand = useToolContentBlockDefaultExpand({
|
||||
configured: hasConfiguredShortcuts,
|
||||
});
|
||||
|
||||
const dispatch = useToolDispatch<ShortCutStruct>();
|
||||
const { emit } = useEvent();
|
||||
|
||||
const [selectedShortcut, setSelectedShortcut] = useState<
|
||||
ShortCutCommand | undefined
|
||||
>(undefined);
|
||||
|
||||
const { run: updateShortcutSortDebounce } = useDebounceFn(
|
||||
async (newShortcuts: string[]) => {
|
||||
await updateShortcutSort(newShortcuts);
|
||||
},
|
||||
{
|
||||
wait: 500,
|
||||
},
|
||||
);
|
||||
|
||||
const onDisorder = async (orderList: ShortCutCommand[]) => {
|
||||
try {
|
||||
const newSortList = orderList.map(item => item.command_id);
|
||||
dispatch({ shortcut_list: orderList, shortcut_sort: newSortList });
|
||||
await updateShortcutSortDebounce(newSortList);
|
||||
} catch (e) {
|
||||
logger.error({
|
||||
error: e as Error,
|
||||
eventName: 'shortcut-disorder-service-fail',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onEditClick = (shortcut: ShortCutCommand) => {
|
||||
setSelectedShortcut(shortcut);
|
||||
openShortcutModal();
|
||||
};
|
||||
|
||||
const onRemoveClick = async (shortcut: ShortCutCommand) => {
|
||||
try {
|
||||
const newSorts = shortcuts
|
||||
?.filter(item => item.command_id !== shortcut.command_id)
|
||||
.map(item => item.command_id);
|
||||
|
||||
await updateShortcutSort(newSorts);
|
||||
const newShortcuts = shortcuts?.filter(
|
||||
item => item.command_id !== shortcut.command_id,
|
||||
);
|
||||
|
||||
newShortcuts && dispatch({ shortcut_list: newShortcuts });
|
||||
} catch (error) {
|
||||
if (!isApiError(error)) {
|
||||
Toast.error(I18n.t('shortcut_modal_fail_to_delete_shortcut_error'));
|
||||
}
|
||||
logger.error({
|
||||
error: error as Error,
|
||||
eventName: 'shortcut-removeShortcut-fail',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
closeShortcutModal();
|
||||
setApiErrorMessage('');
|
||||
};
|
||||
|
||||
const editShortcut = async (
|
||||
shortcut: ShortCutCommand,
|
||||
onFail: () => void,
|
||||
) => {
|
||||
try {
|
||||
await PlaygroundApi.CreateUpdateShortcutCommand(
|
||||
{
|
||||
object_id: botId,
|
||||
space_id: getSpaceId(),
|
||||
shortcuts: shortcut,
|
||||
},
|
||||
{ __disableErrorToast: true },
|
||||
);
|
||||
// TODO: hzf 得加上
|
||||
// if (res && res.data?.check_not_pass) {
|
||||
// Toast.error(I18n.t('shortcut_modal_illegal_keyword_detected_error'));
|
||||
// onFail();
|
||||
// return;
|
||||
// }
|
||||
const newShortcuts = shortcuts?.map(item =>
|
||||
item.command_id === shortcut.command_id ? shortcut : item,
|
||||
);
|
||||
newShortcuts && dispatch({ shortcut_list: newShortcuts });
|
||||
closeModal();
|
||||
onFail();
|
||||
} catch (e) {
|
||||
onFail();
|
||||
if (!isApiError(e)) {
|
||||
Toast.error(I18n.t('shortcut_modal_fail_to_update_shortcut_error'));
|
||||
}
|
||||
if (isApiError(e)) {
|
||||
const error = e as { message?: string; msg?: string };
|
||||
setApiErrorMessage(error.message || error.msg || '');
|
||||
}
|
||||
logger.error({
|
||||
error: e as Error,
|
||||
eventName: 'shortcut-editShortcut-fail',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addShortcut = async (shortcut: ShortCutCommand, onFail: () => void) => {
|
||||
try {
|
||||
const { shortcuts: newShortcut } =
|
||||
await PlaygroundApi.CreateUpdateShortcutCommand(
|
||||
{
|
||||
object_id: botId,
|
||||
space_id: getSpaceId(),
|
||||
shortcuts: shortcut,
|
||||
},
|
||||
{ __disableErrorToast: true },
|
||||
);
|
||||
const strictShortcuts = newShortcut && getStrictShortcuts([newShortcut]);
|
||||
// 一次只能添加一个快捷指令
|
||||
const strictShortcut = strictShortcuts?.[0];
|
||||
if (!strictShortcut) {
|
||||
Toast.error('Please fill in the required fields');
|
||||
return;
|
||||
}
|
||||
const newShortcuts = [
|
||||
...(shortcuts?.map(item => item.command_id) || []),
|
||||
strictShortcut.command_id,
|
||||
];
|
||||
await updateShortcutSort(newShortcuts);
|
||||
dispatch({ shortcut_list: [...(shortcuts || []), ...strictShortcuts] });
|
||||
emit<IToggleContentBlockEventParams>(
|
||||
EventCenterEventName.ToggleContentBlock,
|
||||
{
|
||||
abilityKey: ToolKey.SHORTCUT,
|
||||
isExpand: true,
|
||||
},
|
||||
);
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
onFail();
|
||||
if (!isApiError(error)) {
|
||||
Toast.error(I18n.t('shortcut_modal_fail_to_add_shortcut_error'));
|
||||
}
|
||||
if (isApiError(error)) {
|
||||
const e = error as { message?: string; msg?: string };
|
||||
setApiErrorMessage(e.message || e.msg || '');
|
||||
}
|
||||
logger.error({
|
||||
error: error as Error,
|
||||
eventName: 'shortcut-addShortcut-fail',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
node: ShortcutModal,
|
||||
open: openShortcutModal,
|
||||
close: closeShortcutModal,
|
||||
} = useShortcutEditModal({
|
||||
skillModal: SkillModal,
|
||||
shortcut: selectedShortcut,
|
||||
errorMessage: apiErrorMessage,
|
||||
setErrorMessage: setApiErrorMessage,
|
||||
onAdd: addShortcut,
|
||||
onEdit: editShortcut,
|
||||
botMode,
|
||||
});
|
||||
|
||||
const renderShortcutConfig = () => {
|
||||
if (!hasConfiguredShortcuts) {
|
||||
return <EmptyShortcuts />;
|
||||
}
|
||||
return (
|
||||
<ShortcutList
|
||||
shortcuts={shortcuts}
|
||||
isReadonly={isReadonly}
|
||||
onDisorder={onDisorder}
|
||||
onRemove={onRemoveClick}
|
||||
onEdit={onEditClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{ShortcutModal}
|
||||
<ToolContentBlock
|
||||
className={style['shortcut-tool-config']}
|
||||
showBottomBorder={!hasConfiguredShortcuts}
|
||||
header={title}
|
||||
defaultExpand={defaultExpand}
|
||||
tooltip={<ShortcutTips />}
|
||||
actionButton={
|
||||
!isReadonly && (
|
||||
<>
|
||||
<AddButton
|
||||
tooltips={
|
||||
isReachLimit
|
||||
? I18n.t('bot_ide_shortcut_max_limit', {
|
||||
maxCount: MAX_SHORTCUTS,
|
||||
})
|
||||
: I18n.t('bot_ide_shortcut_add_button')
|
||||
}
|
||||
onClick={() => {
|
||||
if (isReachLimit) {
|
||||
return;
|
||||
}
|
||||
setSelectedShortcut(undefined);
|
||||
openShortcutModal();
|
||||
}}
|
||||
enableAutoHidden={true}
|
||||
data-testid="bot.editor.tool.shortcut.add-button"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
{renderShortcutConfig()}
|
||||
</ToolContentBlock>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import style from '../index.module.less';
|
||||
|
||||
export const EmptyShortcuts: FC = () => (
|
||||
<div className={style['shortcut-config-empty']}>
|
||||
{I18n.t('bot_ide_shortcut_intro')}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { ToolItemList } from '@coze-agent-ide/tool';
|
||||
import { SortableList } from '@coze-studio/components/sortable-list';
|
||||
|
||||
import style from '../index.module.less';
|
||||
import { ShortcutItem } from './shortcut-item';
|
||||
|
||||
interface ShortcutsListProps {
|
||||
shortcuts: ShortCutCommand[];
|
||||
isReadonly: boolean;
|
||||
onRemove?: (shortcut: ShortCutCommand) => void;
|
||||
onDisorder?: (orderList: ShortCutCommand[]) => void;
|
||||
onEdit?: (shortcut: ShortCutCommand) => void;
|
||||
}
|
||||
const SortableListSymbol = Symbol('Shortcut-config-list-sortlist');
|
||||
|
||||
export const ShortcutList: FC<ShortcutsListProps> = props => {
|
||||
const { shortcuts, onDisorder, onEdit, onRemove, isReadonly } = props;
|
||||
const handleRemove = (shortcut: ShortCutCommand) => {
|
||||
onRemove?.(shortcut);
|
||||
};
|
||||
|
||||
const handleDisorder = (orderList: ShortCutCommand[]) => {
|
||||
onDisorder?.(orderList);
|
||||
};
|
||||
|
||||
const handleEdit = (shortcut: ShortCutCommand) => {
|
||||
onEdit?.(shortcut);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cls(style['shortcut-list'])}>
|
||||
<ToolItemList>
|
||||
<SortableList
|
||||
type={SortableListSymbol}
|
||||
list={shortcuts}
|
||||
getId={shortcut => shortcut.command_id}
|
||||
enabled={shortcuts.length > 1 && !isReadonly}
|
||||
onChange={handleDisorder}
|
||||
itemRender={({ data: shortcut, connect, isDragging }) => (
|
||||
<ShortcutItem
|
||||
isDragging={Boolean(isDragging)}
|
||||
connect={connect}
|
||||
key={shortcut.command_id}
|
||||
shortcut={shortcut}
|
||||
isReadonly={isReadonly}
|
||||
onRemove={() => handleRemove(shortcut)}
|
||||
onEdit={() => handleEdit(shortcut)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ToolItemList>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
|
||||
import type { ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import {
|
||||
ToolItem,
|
||||
ToolItemActionEdit,
|
||||
ToolItemActionDelete,
|
||||
ToolItemActionDrag,
|
||||
} from '@coze-agent-ide/tool';
|
||||
import { type ConnectDnd } from '@coze-studio/components';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { UIModal } from '@coze-arch/bot-semi';
|
||||
|
||||
import style from '../index.module.less';
|
||||
import DefaultShortcutIcon from '../../../assets/shortcut-icon-default.svg';
|
||||
|
||||
interface ShortcutItemProps {
|
||||
shortcut: ShortCutCommand;
|
||||
isReadonly: boolean;
|
||||
connect: ConnectDnd;
|
||||
isDragging: boolean;
|
||||
onRemove?: (shortcut: ShortCutCommand) => void;
|
||||
onEdit?: (shortcut: ShortCutCommand) => void;
|
||||
onDisorder?: (order: number) => void;
|
||||
}
|
||||
|
||||
export const ShortcutItem: FC<ShortcutItemProps> = ({
|
||||
shortcut,
|
||||
onEdit,
|
||||
onRemove,
|
||||
connect,
|
||||
isReadonly,
|
||||
isDragging,
|
||||
}) => {
|
||||
const dropRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef<HTMLDivElement>(null);
|
||||
connect(dropRef, dragRef);
|
||||
useEffect(() => {
|
||||
connect(dropRef, dragRef);
|
||||
}, [dragRef, dropRef]);
|
||||
// 点击删除,弹出二次确认弹窗
|
||||
const openConfirmRemoveModal = () => {
|
||||
UIModal.info({
|
||||
title: I18n.t('bot_ide_shortcut_removal_confirm'),
|
||||
width: 320,
|
||||
icon: null,
|
||||
closeIcon: <></>,
|
||||
className: style['delete-modal'],
|
||||
cancelText: I18n.t('Cancel'),
|
||||
okText: I18n.t('Remove'),
|
||||
cancelButtonProps: { className: style['delete-modal-cancel-button'] },
|
||||
okButtonProps: {
|
||||
className: style['delete-modal-ok-button'],
|
||||
},
|
||||
onOk: () => onRemove?.(shortcut),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={dropRef}>
|
||||
<ToolItem
|
||||
title={shortcut.command_name ?? ''}
|
||||
description={shortcut.description ?? ''}
|
||||
avatar={shortcut.shortcut_icon?.url || DefaultShortcutIcon}
|
||||
avatarStyle={{
|
||||
padding: '10px',
|
||||
background: '#fff',
|
||||
}}
|
||||
actions={
|
||||
<>
|
||||
<div ref={dragRef}>
|
||||
<ToolItemActionDrag
|
||||
data-testid="chat-area.shortcut.drag-button"
|
||||
isDragging={isDragging}
|
||||
disabled={isReadonly}
|
||||
/>
|
||||
</div>
|
||||
<ToolItemActionEdit
|
||||
tooltips={I18n.t('bot_ide_shortcut_item_edit')}
|
||||
onClick={() => onEdit?.(shortcut)}
|
||||
data-testid="chat-area.shortcut.edit-button"
|
||||
disabled={isReadonly}
|
||||
/>
|
||||
<ToolItemActionDelete
|
||||
tooltips={I18n.t('bot_ide_shortcut_item_trash')}
|
||||
onClick={() => openConfirmRemoveModal()}
|
||||
disabled={isReadonly}
|
||||
data-testid="chat-area.shortcut.delete-button"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useRef, useState } from 'react';
|
||||
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { Deferred } from '@coze-common/chat-area-utils';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { UIModal } from '@coze-arch/bot-semi';
|
||||
|
||||
export interface HasUnusedComponentsConfirmModalProps {
|
||||
onConfirm?: () => void;
|
||||
onCancel?: () => void;
|
||||
components: ShortCutCommand['components_list'];
|
||||
}
|
||||
export const HasUnusedComponentsConfirmModal: FC<
|
||||
HasUnusedComponentsConfirmModalProps
|
||||
> = ({ onConfirm, components, onCancel }) => {
|
||||
const unUsedComponentsNames = components
|
||||
?.map(component => component.name)
|
||||
.join(', ');
|
||||
return (
|
||||
<UIModal
|
||||
visible
|
||||
footer={null}
|
||||
onCancel={onCancel}
|
||||
bodyStyle={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
padding: '0 0 16px 0',
|
||||
}}
|
||||
title={I18n.t(
|
||||
'shortcut_modal_save_shortcut_with_components_unused_modal_title',
|
||||
)}
|
||||
>
|
||||
<div className="pb-6">
|
||||
{I18n.t(
|
||||
'shortcut_modal_save_shortcut_with_components_unused_modal_desc',
|
||||
{
|
||||
unUsedComponentsNames,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
color="highlight"
|
||||
className="!coz-mg-hglt !coz-fg-hglt"
|
||||
>
|
||||
{I18n.t('Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{I18n.t('Confirm')}</Button>
|
||||
</div>
|
||||
</UIModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const useHasUnusedComponentsConfirmModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [components, setComponents] = useState<
|
||||
ShortCutCommand['components_list']
|
||||
>([]);
|
||||
|
||||
const openDeferred = useRef<Deferred<boolean> | null>(null);
|
||||
|
||||
const close = () => {
|
||||
openDeferred.current?.resolve(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
openDeferred.current?.resolve(true);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const open = (unUsedComponents: ShortCutCommand['components_list']) => {
|
||||
openDeferred.current = new Deferred<boolean>();
|
||||
setComponents(unUsedComponents);
|
||||
setVisible(true);
|
||||
return openDeferred.current.promise;
|
||||
};
|
||||
|
||||
return {
|
||||
node: visible ? (
|
||||
<HasUnusedComponentsConfirmModal
|
||||
components={components}
|
||||
onCancel={close}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
) : null,
|
||||
close,
|
||||
open,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type FC,
|
||||
forwardRef,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { type Form } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
type shortcut_command,
|
||||
ToolType,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import VarQueryTextareaWrapperWithField from '../var-query-textarea/field';
|
||||
import { ComponentsTable } from '../components-table';
|
||||
import type { ComponentsTableActions } from '../components-table';
|
||||
import type {
|
||||
ShortcutEditFormValues,
|
||||
SkillsModalProps,
|
||||
ToolInfo,
|
||||
} from '../../types';
|
||||
import { getDSLFromComponents } from '../../../utils/dsl-template';
|
||||
import { SkillSwitch } from './skill-switch';
|
||||
import { getUnusedComponents, initComponentsByToolParams } from './method';
|
||||
import { useHasUnusedComponentsConfirmModal } from './confirm-modal';
|
||||
|
||||
export interface IActionSwitchAreaProps {
|
||||
skillModal: FC<SkillsModalProps>;
|
||||
editedShortcut: ShortcutEditFormValues;
|
||||
formRef: RefObject<Form>;
|
||||
modalRef?: RefObject<HTMLDivElement>;
|
||||
isBanned: boolean;
|
||||
}
|
||||
|
||||
export interface IActionSwitchAreaRef {
|
||||
getValues: () => ShortcutEditFormValues;
|
||||
validate: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const ActionSwitchArea = forwardRef<
|
||||
IActionSwitchAreaRef,
|
||||
IActionSwitchAreaProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
editedShortcut,
|
||||
skillModal: SkillModal,
|
||||
formRef,
|
||||
isBanned,
|
||||
modalRef,
|
||||
} = props;
|
||||
|
||||
const useTool = editedShortcut?.use_tool ?? false;
|
||||
|
||||
const initialComponents = editedShortcut?.components_list?.length
|
||||
? editedShortcut.components_list
|
||||
: [];
|
||||
|
||||
const [components, setComponents] =
|
||||
useState<shortcut_command.Components[]>(initialComponents);
|
||||
|
||||
const componentsRef = useRef<{
|
||||
formApi?: ComponentsTableActions;
|
||||
}>(null);
|
||||
|
||||
const { open: openConfirmModal, node: ConfirmModal } =
|
||||
useHasUnusedComponentsConfirmModal();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getValues: () => {
|
||||
const values = formRef.current?.formApi.getValues();
|
||||
return {
|
||||
...values,
|
||||
components_list: components,
|
||||
use_tool: useTool,
|
||||
card_schema: getDSLFromComponents(components),
|
||||
};
|
||||
},
|
||||
validate: async () => {
|
||||
if (!formRef.current) {
|
||||
return false;
|
||||
}
|
||||
return await checkComponentsValid();
|
||||
},
|
||||
}));
|
||||
|
||||
const onToolParamsChange = (toolInfo: ToolInfo | null) => {
|
||||
const {
|
||||
tool_type,
|
||||
plugin_id,
|
||||
plugin_api_name,
|
||||
api_id,
|
||||
tool_name,
|
||||
work_flow_id,
|
||||
tool_params_list = [],
|
||||
} = toolInfo || {};
|
||||
const newComponents = initComponentsByToolParams(tool_params_list);
|
||||
// TODO: hzf, 有点复杂,看看可以initValue么
|
||||
formRef.current?.formApi.setValue('components_list', newComponents);
|
||||
setComponents(newComponents);
|
||||
// 只有这种情况需要手动更新数据
|
||||
componentsRef.current?.formApi?.setValues(newComponents);
|
||||
|
||||
formRef.current?.formApi.setValue('tool_type', tool_type);
|
||||
formRef.current?.formApi.setValue('plugin_id', plugin_id);
|
||||
tool_type === ToolType.ToolTypeWorkFlow &&
|
||||
formRef.current?.formApi.setValue('work_flow_id', work_flow_id);
|
||||
formRef.current?.formApi.setValue('plugin_api_name', plugin_api_name);
|
||||
formRef.current?.formApi.setValue('plugin_api_id', api_id);
|
||||
formRef.current?.formApi.setValue('tool_info', {
|
||||
tool_name,
|
||||
tool_params_list,
|
||||
});
|
||||
};
|
||||
|
||||
const checkComponentsValid = async (): Promise<boolean> => {
|
||||
if (!formRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await componentsRef.current?.formApi?.validate();
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch -- form validate
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const componentNotUsed = getUnusedComponents(editedShortcut);
|
||||
if (componentNotUsed.length) {
|
||||
return await openConfirmModal(componentNotUsed);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.formApi.setValue('components_list', components);
|
||||
}, [components]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkillSwitch
|
||||
skillModal={SkillModal}
|
||||
isBanned={isBanned}
|
||||
onToolChange={onToolParamsChange}
|
||||
editedShortcut={editedShortcut}
|
||||
/>
|
||||
<ComponentsTable
|
||||
toolType={useTool ? ToolType.ToolTypePlugin : undefined}
|
||||
toolInfo={editedShortcut?.tool_info ?? {}}
|
||||
ref={componentsRef}
|
||||
disabled={isBanned}
|
||||
components={components}
|
||||
onChange={newComponents => {
|
||||
setComponents(newComponents);
|
||||
}}
|
||||
/>
|
||||
<VarQueryTextareaWrapperWithField
|
||||
field="template_query"
|
||||
value={editedShortcut?.template_query || ''}
|
||||
components={components}
|
||||
modalRef={modalRef}
|
||||
/>
|
||||
{ConfirmModal}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
InputType,
|
||||
type shortcut_command,
|
||||
type ToolParams,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type ShortcutEditFormValues } from '../../types';
|
||||
|
||||
export const initComponentsByToolParams = (
|
||||
params: ToolParams[],
|
||||
): shortcut_command.Components[] =>
|
||||
params?.map(param => {
|
||||
const { name, desc, refer_component } = param;
|
||||
return {
|
||||
name,
|
||||
parameter: name,
|
||||
description: desc,
|
||||
input_type: InputType.TextInput,
|
||||
default_value: {
|
||||
value: '',
|
||||
},
|
||||
hide: !refer_component,
|
||||
};
|
||||
});
|
||||
|
||||
// 获取没有被使用的组件
|
||||
export const getUnusedComponents = (
|
||||
shortcut: ShortcutEditFormValues,
|
||||
): shortcut_command.Components[] => {
|
||||
const { components_list, template_query } = shortcut;
|
||||
return (
|
||||
components_list?.filter(
|
||||
component => !template_query?.includes(`{{${component.name}}}`),
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
|
||||
> svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
|
||||
import style from '../../index.module.less';
|
||||
import FieldLabel from '../../components/field-label';
|
||||
import {
|
||||
type ShortcutEditFormValues,
|
||||
type SkillsModalProps,
|
||||
type ToolInfo,
|
||||
} from '../../../types';
|
||||
import { getToolInfoByShortcut } from '../../../../utils/tool-params';
|
||||
import { useToolAction } from './tool-action';
|
||||
|
||||
export interface ChooseSendTypeRadioProps {
|
||||
editedShortcut?: ShortcutEditFormValues;
|
||||
skillModal: FC<SkillsModalProps>;
|
||||
isBanned: boolean;
|
||||
onToolChange?: (tooInfo: ToolInfo | null) => void;
|
||||
}
|
||||
|
||||
const { Checkbox } = Form;
|
||||
|
||||
export const SkillSwitch: FC<ChooseSendTypeRadioProps> = props => {
|
||||
const { editedShortcut, skillModal, isBanned, onToolChange } = props;
|
||||
const { action, open, cancel } = useToolAction({
|
||||
initTool: getToolInfoByShortcut(editedShortcut),
|
||||
onSelect: onToolChange,
|
||||
skillModal,
|
||||
isBanned,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
style['form-item'],
|
||||
style['shortcut-action-item'],
|
||||
'pb-[16px]',
|
||||
)}
|
||||
>
|
||||
<FieldLabel>{I18n.t('shortcut_modal_skill')}</FieldLabel>
|
||||
<div className="flex items-center justify-between h-[32px]">
|
||||
<Checkbox
|
||||
field="use_tool"
|
||||
onChange={e => {
|
||||
const { checked } = e.target;
|
||||
checked ? open() : cancel();
|
||||
}}
|
||||
noLabel
|
||||
fieldClassName="!pb-0"
|
||||
>
|
||||
{I18n.t('shortcut_modal_shortcut_action_use_plugin_wf')}
|
||||
</Checkbox>
|
||||
<div className="flex items-center">
|
||||
{editedShortcut?.use_tool ? action : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// format选择的插件参数列表
|
||||
import { type WorkFlowItemType } from '@coze-studio/bot-detail-store';
|
||||
import { type PluginApi, ToolType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type ToolInfo } from '../../../types';
|
||||
|
||||
// 最多勾选10个,如果入参数量超过10个,仅勾选其中10个,优先勾选required参数;勾满10个时,其他checkbox置灰不可继续勾选。
|
||||
export const MAX_TOOL_PARAMS_COUNT = 10;
|
||||
|
||||
// 初始化工具列表参数
|
||||
export const initToolInfoByToolApi = (
|
||||
toolApi?: WorkFlowItemType | PluginApi,
|
||||
): ToolInfo | null => {
|
||||
if (!toolApi) {
|
||||
return null;
|
||||
}
|
||||
const isWorkflow = 'workflow_id' in toolApi;
|
||||
|
||||
const workflowPluginProcessedToolInfo = isWorkflow
|
||||
? initToolInfoByWorkFlow(toolApi as WorkFlowItemType)
|
||||
: initToolInfoByPlugin(toolApi);
|
||||
|
||||
const { tool_params_list } = workflowPluginProcessedToolInfo;
|
||||
|
||||
// 对params进行排序,将required=true的字段排在前面
|
||||
const sortedParams = tool_params_list?.sort(
|
||||
(a, b) => (b.required ? 1 : -1) - (a.required ? 1 : -1),
|
||||
);
|
||||
|
||||
return {
|
||||
...workflowPluginProcessedToolInfo,
|
||||
tool_params_list:
|
||||
sortedParams?.map((param, index) => {
|
||||
const { name, desc, required, type } = param;
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
desc,
|
||||
required,
|
||||
default_value: '',
|
||||
refer_component: index < MAX_TOOL_PARAMS_COUNT,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
};
|
||||
|
||||
// workflow参数转化为toolParams
|
||||
export const initToolInfoByWorkFlow = (
|
||||
workFlow: WorkFlowItemType,
|
||||
): ToolInfo => {
|
||||
const { name, parameters, workflow_id, ...rest } = workFlow;
|
||||
return {
|
||||
...rest,
|
||||
tool_type: ToolType.ToolTypeWorkFlow,
|
||||
tool_name: name,
|
||||
plugin_api_name: name,
|
||||
tool_params_list: parameters || [],
|
||||
work_flow_id: workflow_id,
|
||||
};
|
||||
};
|
||||
|
||||
export const initToolInfoByPlugin = (plugin: PluginApi): ToolInfo => {
|
||||
const { name, plugin_name, parameters, ...rest } = plugin;
|
||||
return {
|
||||
...rest,
|
||||
tool_type: ToolType.ToolTypePlugin,
|
||||
tool_name: plugin_name ?? '',
|
||||
plugin_api_name: name,
|
||||
tool_params_list: parameters || [],
|
||||
};
|
||||
};
|
||||
|
||||
// 获取skillModal开启的tab
|
||||
export const getSkillModalTab = (): (
|
||||
| 'plugin'
|
||||
| 'workflow'
|
||||
| 'imageFlow'
|
||||
| 'datasets'
|
||||
)[] => ['plugin', 'workflow'];
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC, useState } from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { type WorkFlowItemType } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
Typography,
|
||||
Toast,
|
||||
Popover,
|
||||
UIButton,
|
||||
Tooltip,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconAdd,
|
||||
IconInfo,
|
||||
IconPluginsSelected,
|
||||
IconWorkflowsSelected,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import {
|
||||
type PluginApi,
|
||||
type PluginParameter,
|
||||
ToolType,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import style from '../../index.module.less';
|
||||
import ActionButton from '../../components/action-button';
|
||||
import {
|
||||
OpenModeType,
|
||||
type SkillsModalProps,
|
||||
type ToolInfo,
|
||||
} from '../../../types';
|
||||
import { validatePluginAndWorkflowParams } from '../../../../utils/tool-params';
|
||||
import CloseToolIcon from '../../../../assets/close-tool.svg';
|
||||
import { getSkillModalTab, initToolInfoByToolApi } from './method';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ToolActionProps {
|
||||
initTool?: ToolInfo;
|
||||
skillModal: FC<SkillsModalProps>;
|
||||
onSelect?: (tooInfo: ToolInfo | null) => void;
|
||||
isBanned: boolean;
|
||||
}
|
||||
|
||||
const ToolButton = (props: {
|
||||
toolInfo: ToolInfo;
|
||||
onCancel: () => void;
|
||||
isBanned: boolean;
|
||||
}) => {
|
||||
const {
|
||||
toolInfo: { tool_type, tool_name },
|
||||
onCancel,
|
||||
isBanned,
|
||||
} = props;
|
||||
const [removePopoverVisible, setRemovePopoverVisible] = useState(false);
|
||||
const removePopoverContent = (
|
||||
<div className={style['remove-popover-content']}>
|
||||
<Typography.Text className={style.title}>
|
||||
{I18n.t('shortcut_modal_remove_plugin_wf_double_confirm')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={style.desc}>
|
||||
{I18n.t('shortcut_modal_remove_plugin_wf_double_tip')}
|
||||
</Typography.Text>
|
||||
<UIButton className={style['delete-btn']} onClick={() => onCancel()}>
|
||||
{I18n.t('shortcut_modal_remove_plugin_wf_button')}
|
||||
</UIButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
position="bottomRight"
|
||||
content={removePopoverContent}
|
||||
onClickOutSide={() => setRemovePopoverVisible(false)}
|
||||
visible={removePopoverVisible}
|
||||
>
|
||||
<div
|
||||
className={cs(
|
||||
'flex ml-2 rounded-[6px] coz-mg-primary items-center px-[10px] py-[3px] text-xs coz-fg-primary',
|
||||
)}
|
||||
>
|
||||
{tool_type === ToolType.ToolTypePlugin && (
|
||||
<IconPluginsSelected className={styles.icon} />
|
||||
)}
|
||||
{tool_type === ToolType.ToolTypeWorkFlow && (
|
||||
<IconWorkflowsSelected className={styles.icon} />
|
||||
)}
|
||||
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: tool_name },
|
||||
},
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{tool_name}
|
||||
</Typography.Text>
|
||||
{isBanned ? (
|
||||
<Tooltip content={I18n.t('Plugin_delisted')}>
|
||||
<IconInfo className="ml-1" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<img
|
||||
className="ml-[8px] cursor-pointer"
|
||||
alt="close"
|
||||
src={CloseToolIcon}
|
||||
onClick={() => {
|
||||
setRemovePopoverVisible(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export const useToolAction = (props: ToolActionProps) => {
|
||||
const { skillModal: SkillModal, onSelect, initTool, isBanned } = props;
|
||||
const [skillModalVisible, setSkillModalVisible] = useState(false);
|
||||
const [selectedTool, setSelectedTool] = useState<ToolInfo | null>(
|
||||
initTool || null,
|
||||
);
|
||||
const onToolChange = (toolApi: WorkFlowItemType | PluginApi | undefined) => {
|
||||
const tooInfo = initToolInfoByToolApi(toolApi);
|
||||
const { tool_params_list } = tooInfo || {};
|
||||
if (!checkParams(tool_params_list ?? [])) {
|
||||
return;
|
||||
}
|
||||
onSelect?.(tooInfo);
|
||||
setSelectedTool(tooInfo);
|
||||
setSkillModalVisible(false);
|
||||
};
|
||||
const checkParams = (parameters: Array<PluginParameter>) => {
|
||||
const { isSuccess, inValidType } = validatePluginAndWorkflowParams(
|
||||
parameters ?? [],
|
||||
true,
|
||||
);
|
||||
|
||||
if (isSuccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (inValidType === 'empty') {
|
||||
Toast.error(I18n.t('shortcut_modal_add_plugin_wf_no_input_error'));
|
||||
} else {
|
||||
Toast.error(I18n.t('shortcut_modal_add_plugin_wf_complex_input_error'));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
onSelect?.(null);
|
||||
setSkillModalVisible(true);
|
||||
};
|
||||
const cancel = () => {
|
||||
onSelect?.(null);
|
||||
setSelectedTool(null);
|
||||
};
|
||||
|
||||
const action = (
|
||||
<div className="mr-2 mt-[-2px]">
|
||||
{selectedTool?.tool_type ? (
|
||||
<ToolButton
|
||||
toolInfo={selectedTool}
|
||||
onCancel={cancel}
|
||||
isBanned={isBanned}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton icon={<IconAdd />} onClick={open}>
|
||||
{I18n.t('shortcut_modal_use_tool_select_button')}
|
||||
</ActionButton>
|
||||
)}
|
||||
{skillModalVisible ? (
|
||||
<SkillModal
|
||||
tabs={getSkillModalTab()}
|
||||
onCancel={() => setSkillModalVisible(false)}
|
||||
openMode={OpenModeType.OnlyOnceAdd}
|
||||
openModeCallback={onToolChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return {
|
||||
action,
|
||||
open,
|
||||
cancel,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useToolStore } from '@coze-agent-ide/tool';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type ShortcutFileInfo } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { FormInputWithMaxCount } from '../components';
|
||||
import { type ShortcutEditFormValues } from '../../types';
|
||||
import { validateButtonNameRepeat } from '../../../utils/tool-params';
|
||||
import { ShortcutIconField } from './shortcut-icon';
|
||||
|
||||
export interface ButtonNameProps {
|
||||
editedShortcut: ShortcutEditFormValues;
|
||||
}
|
||||
|
||||
export const ButtonName: FC<ButtonNameProps> = props => {
|
||||
const { existedShortcuts } = useToolStore(
|
||||
useShallow(state => ({
|
||||
existedShortcuts: state.shortcut.shortcut_list,
|
||||
})),
|
||||
);
|
||||
const { editedShortcut } = props;
|
||||
const [selectIcon, setSelectIcon] = useState<ShortcutFileInfo | undefined>(
|
||||
editedShortcut.shortcut_icon,
|
||||
);
|
||||
|
||||
return (
|
||||
<FormInputWithMaxCount
|
||||
className="p-1"
|
||||
field="command_name"
|
||||
placeholder={I18n.t('shortcut_modal_button_name_input_placeholder')}
|
||||
prefix={
|
||||
<ShortcutIconField
|
||||
iconInfo={selectIcon}
|
||||
field="shortcut_icon"
|
||||
noLabel
|
||||
fieldClassName="!pb-0"
|
||||
onLoadList={list => {
|
||||
// 如果是编辑状态,不设置默认icon, 新增下默认选中列表第一个icon
|
||||
const isEdit = !!editedShortcut.command_id;
|
||||
if (isEdit) {
|
||||
return;
|
||||
}
|
||||
const defaultIcon = list.at(0);
|
||||
defaultIcon && setSelectIcon(defaultIcon);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
suffix={<></>}
|
||||
maxCount={20}
|
||||
maxLength={20}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('shortcut_modal_button_name_is_required'),
|
||||
},
|
||||
{
|
||||
validator: (rule, value) =>
|
||||
validateButtonNameRepeat(
|
||||
{
|
||||
...editedShortcut,
|
||||
command_name: value,
|
||||
},
|
||||
existedShortcuts ?? [],
|
||||
),
|
||||
message: I18n.t('shortcut_modal_button_name_conflict_error'),
|
||||
},
|
||||
]}
|
||||
noLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozWarningCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Skeleton } from '@coze-arch/bot-semi';
|
||||
import { type FileInfo } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { Icon } from './icon';
|
||||
|
||||
const SINGLE_LINE_LOADING_COUNT = 10;
|
||||
|
||||
export interface IconListProps {
|
||||
list: FileInfo[];
|
||||
initValue?: FileInfo;
|
||||
onSelect: (item: FileInfo) => void;
|
||||
onClear: (item: FileInfo) => void;
|
||||
}
|
||||
export const IconList: FC<IconListProps> = props => {
|
||||
const { list, onSelect, onClear, initValue } = props;
|
||||
const [selectIcon, setSelectIcon] = useState<FileInfo | undefined>(initValue);
|
||||
const onIconClick = (item: FileInfo) => {
|
||||
const { url } = item;
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
if (url === selectIcon?.url) {
|
||||
setSelectIcon(undefined);
|
||||
onClear(item);
|
||||
return;
|
||||
}
|
||||
setSelectIcon(item);
|
||||
onSelect(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 p-4">
|
||||
{list.map((item, index) => (
|
||||
<div onClick={() => onIconClick?.(item)}>
|
||||
<Icon
|
||||
key={index}
|
||||
icon={item}
|
||||
className={cls({
|
||||
'coz-mg-secondary-pressed': item.uri === selectIcon?.uri,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnimateLoading = () => (
|
||||
<>
|
||||
<SingleLoading />
|
||||
<SingleLoading />
|
||||
<SingleLoading />
|
||||
</>
|
||||
);
|
||||
|
||||
const SingleLoading = () => (
|
||||
<div>
|
||||
<Skeleton
|
||||
active
|
||||
loading
|
||||
placeholder={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: SINGLE_LINE_LOADING_COUNT }).map((_, index) => (
|
||||
<Skeleton.Image
|
||||
key={index}
|
||||
style={{
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IconListField = () => (
|
||||
<div className="flex justify-center items-center flex-col w-[420px] h-[148px]">
|
||||
<IconCozWarningCircle className="mb-4 w-8 h-8 coz-fg-hglt-red" />
|
||||
<div className="coz-fg-secondary text-xs">
|
||||
{/*@ts-expect-error --替换*/}
|
||||
{I18n.t('Connection failed')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { type FileInfo } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import DefaultIcon from '../../../../assets/shortcut-icon-default.svg';
|
||||
|
||||
export interface ShortcutIconProps {
|
||||
icon?: FileInfo;
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
const DEFAULT_ICON_SIZE = 28;
|
||||
|
||||
const DefaultIconInfo = {
|
||||
url: DefaultIcon,
|
||||
};
|
||||
|
||||
export const Icon: FC<ShortcutIconProps> = props => {
|
||||
const { icon, width, height, className } = props;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
className={cls(
|
||||
'rounded-[6px] p-1 coz-mg-primary hover:coz-mg-secondary-hovered mr-1 cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
width: width ?? DEFAULT_ICON_SIZE,
|
||||
height: height ?? DEFAULT_ICON_SIZE,
|
||||
}}
|
||||
alt="icon"
|
||||
src={icon?.url || DefaultIconInfo.url}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import { Popover, withField } from '@coze-arch/bot-semi';
|
||||
import { type FileInfo } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import DefaultIcon from '../../../../assets/shortcut-icon-default.svg';
|
||||
import { useGetIconList } from './use-get-icon-list';
|
||||
import { IconList, AnimateLoading, IconListField } from './icon-list';
|
||||
import { Icon } from './icon';
|
||||
|
||||
export interface ShortcutIconProps {
|
||||
iconInfo?: FileInfo;
|
||||
onChange?: (iconInfo: FileInfo | undefined) => void;
|
||||
onLoadList?: (list: FileInfo[]) => void;
|
||||
}
|
||||
|
||||
const DefaultIconInfo = {
|
||||
url: DefaultIcon,
|
||||
};
|
||||
export const ShortcutIcon: FC<ShortcutIconProps> = props => {
|
||||
const { iconInfo: initIconInfo, onChange, onLoadList } = props;
|
||||
const [iconListVisible, setIconListVisible] = useState(false);
|
||||
const { iconList, loading, error } = useGetIconList();
|
||||
const [selectIcon, setSelectIcon] = useState(
|
||||
initIconInfo?.url ? initIconInfo : DefaultIconInfo,
|
||||
);
|
||||
const onSelectIcon = (item: FileInfo) => {
|
||||
const { url } = item;
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
setSelectIcon(item);
|
||||
setIconListVisible(false);
|
||||
onChange?.(item);
|
||||
};
|
||||
|
||||
const onClearIcon = () => {
|
||||
setSelectIcon(DefaultIcon);
|
||||
setIconListVisible(false);
|
||||
onChange?.(undefined);
|
||||
};
|
||||
|
||||
const IconListRender = () => {
|
||||
if (error) {
|
||||
return <IconListField />;
|
||||
}
|
||||
if (loading) {
|
||||
return <AnimateLoading />;
|
||||
}
|
||||
return (
|
||||
<IconList
|
||||
initValue={selectIcon}
|
||||
list={iconList}
|
||||
onSelect={onSelectIcon}
|
||||
onClear={onClearIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
onLoadList?.(iconList);
|
||||
}, [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
initIconInfo && onSelectIcon(initIconInfo);
|
||||
}, [initIconInfo]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
visible={iconListVisible}
|
||||
onClickOutSide={() => setIconListVisible(false)}
|
||||
position="bottomLeft"
|
||||
spacing={{
|
||||
x: 0,
|
||||
y: 10,
|
||||
}}
|
||||
content={IconListRender()}
|
||||
>
|
||||
<div
|
||||
className="flex items-center"
|
||||
onClick={() => setIconListVisible(true)}
|
||||
>
|
||||
<Icon
|
||||
icon={selectIcon}
|
||||
width={22}
|
||||
height={24}
|
||||
className={cls({
|
||||
'coz-mg-secondary-pressed': iconListVisible,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShortcutIconField: FC<ShortcutIconProps & CommonFieldProps> =
|
||||
withField(ShortcutIcon);
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { GetFileUrlsScene } from '@coze-arch/bot-api/playground_api';
|
||||
import { PlaygroundApi } from '@coze-arch/bot-api';
|
||||
export const useGetIconList = () => {
|
||||
const { data, loading, error } = useRequest(
|
||||
async () =>
|
||||
await PlaygroundApi.GetFileUrls({
|
||||
scene: GetFileUrlsScene.shorcutIcon,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
iconList: data?.file_list ?? [],
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
type SelectComponentTypeItem,
|
||||
type TextComponentTypeItem,
|
||||
type UploadComponentTypeItem,
|
||||
} from '../types';
|
||||
import { type UploadItemType } from '../../../../utils/file-const';
|
||||
import { UploadField } from './upload-field';
|
||||
import { SelectWithInputTypeField } from './select-field';
|
||||
import { InputWithInputTypeField } from './input-field';
|
||||
|
||||
export interface ComponentDefaultChangeValue {
|
||||
type: InputType.TextInput | UploadItemType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ComponentDefaultValueProps {
|
||||
field: string;
|
||||
componentType:
|
||||
| TextComponentTypeItem
|
||||
| SelectComponentTypeItem
|
||||
| UploadComponentTypeItem;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ComponentDefaultValue: FC<ComponentDefaultValueProps> = props => {
|
||||
const { componentType, field, disabled = false } = props;
|
||||
const { type } = componentType;
|
||||
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<InputWithInputTypeField
|
||||
noLabel
|
||||
value={{
|
||||
type: InputType.TextInput,
|
||||
value: '',
|
||||
}}
|
||||
field={field}
|
||||
noErrorMessage
|
||||
placeholder={I18n.t(
|
||||
'shortcut_modal_use_tool_parameter_default_value_placeholder',
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === 'select') {
|
||||
return (
|
||||
<SelectWithInputTypeField
|
||||
value={{
|
||||
type: InputType.TextInput,
|
||||
value: '',
|
||||
}}
|
||||
noLabel
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
field={field}
|
||||
noErrorMessage
|
||||
optionList={componentType.options.map(option => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === 'upload') {
|
||||
// 先置灰,后续放开上传默认值
|
||||
return <UploadField />;
|
||||
// return (
|
||||
// <UploadDefaultValue
|
||||
// noLabel
|
||||
// field={field}
|
||||
// acceptUploadItemTypes={componentType.uploadTypes}
|
||||
// uploadItemConfig={{
|
||||
// [InputType.UploadImage]: {
|
||||
// maxSize: IMAGE_MAX_SIZE,
|
||||
// },
|
||||
// [InputType.UploadDoc]: {
|
||||
// maxSize: FILE_MAX_SIZE,
|
||||
// },
|
||||
// [InputType.UploadTable]: {
|
||||
// maxSize: FILE_MAX_SIZE,
|
||||
// },
|
||||
// [InputType.UploadAudio]: {
|
||||
// maxSize: FILE_MAX_SIZE,
|
||||
// },
|
||||
// }}
|
||||
// onChange={res => {
|
||||
// const { default_value, default_value_type } = res
|
||||
// ? convertComponentDefaultValueToFormValues(res)
|
||||
// : {
|
||||
// default_value: '',
|
||||
// default_value_type: undefined,
|
||||
// };
|
||||
// return {
|
||||
// value: default_value,
|
||||
// type: default_value_type,
|
||||
// };
|
||||
// }}
|
||||
// uploadFile={({ file, onError, onProgress, onSuccess }) => {
|
||||
// getRegisteredPluginInstance?.({
|
||||
// file,
|
||||
// onProgress,
|
||||
// onError,
|
||||
// onSuccess,
|
||||
// });
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import type { InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import { UIInput, withField } from '@coze-arch/bot-semi';
|
||||
import { InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
type InputWithInputTypeProps = {
|
||||
value?: { type: InputType; value: string };
|
||||
onChange?: (value: { type: InputType; value: string }) => void;
|
||||
} & Omit<InputProps, 'value'>;
|
||||
|
||||
const MaxLength = 100;
|
||||
|
||||
const InputWithInputType: FC<InputWithInputTypeProps> = props => {
|
||||
const { value, onChange, ...rest } = props;
|
||||
return (
|
||||
<UIInput
|
||||
value={value?.value}
|
||||
{...rest}
|
||||
maxLength={MaxLength}
|
||||
onChange={inputValue => {
|
||||
const newValue = {
|
||||
type: value?.type || InputType.TextInput,
|
||||
value: inputValue,
|
||||
};
|
||||
onChange?.(newValue);
|
||||
return newValue;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputWithInputTypeField: FC<
|
||||
InputWithInputTypeProps & CommonFieldProps
|
||||
> = withField(InputWithInputType, {
|
||||
valueKey: 'value',
|
||||
onKeyChangeFnName: 'onChange',
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FileTypeEnum, getFileInfo } from '@coze-common/chat-core';
|
||||
import { shortcut_command } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type UploadItemConfig } from '../types';
|
||||
import { acceptMap, type UploadItemType } from '../../../../utils/file-const';
|
||||
type FileTypeEnumWithoutDefault = Exclude<
|
||||
FileTypeEnum,
|
||||
FileTypeEnum.DEFAULT_UNKNOWN
|
||||
>;
|
||||
|
||||
const fileTypeToInputTypeMap: {
|
||||
[key in FileTypeEnumWithoutDefault]: UploadItemType;
|
||||
} = {
|
||||
[FileTypeEnum.IMAGE]: shortcut_command.InputType.UploadImage,
|
||||
[FileTypeEnum.AUDIO]: shortcut_command.InputType.UploadAudio,
|
||||
[FileTypeEnum.PDF]: shortcut_command.InputType.UploadDoc,
|
||||
[FileTypeEnum.DOCX]: shortcut_command.InputType.UploadDoc,
|
||||
[FileTypeEnum.EXCEL]: shortcut_command.InputType.UploadTable,
|
||||
[FileTypeEnum.CSV]: shortcut_command.InputType.UploadTable,
|
||||
[FileTypeEnum.VIDEO]: shortcut_command.InputType.VIDEO,
|
||||
[FileTypeEnum.PPT]: shortcut_command.InputType.PPT,
|
||||
[FileTypeEnum.TXT]: shortcut_command.InputType.TXT,
|
||||
[FileTypeEnum.ARCHIVE]: shortcut_command.InputType.ARCHIVE,
|
||||
[FileTypeEnum.CODE]: shortcut_command.InputType.CODE,
|
||||
};
|
||||
|
||||
export const getFileTypeFromInputType = (
|
||||
inputType: shortcut_command.InputType,
|
||||
) => {
|
||||
for (const [fileType, type] of Object.entries(fileTypeToInputTypeMap)) {
|
||||
if (type === inputType) {
|
||||
return fileType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getInputTypeFromFileType = (
|
||||
fileType: FileTypeEnumWithoutDefault,
|
||||
) => fileTypeToInputTypeMap[fileType];
|
||||
|
||||
export const getInputTypeFromFile = (file: File): UploadItemType | '' => {
|
||||
const fileInfo = getFileInfo(file);
|
||||
const fileType = fileInfo?.fileType;
|
||||
if (!fileInfo) {
|
||||
return '';
|
||||
}
|
||||
if (!fileType || fileType === FileTypeEnum.DEFAULT_UNKNOWN) {
|
||||
return '';
|
||||
}
|
||||
return getInputTypeFromFileType(fileType);
|
||||
};
|
||||
|
||||
// 判断文件是否超过最大限制
|
||||
export const isOverMaxSizeByUploadItemConfig = (
|
||||
file: File | undefined,
|
||||
config: UploadItemConfig | undefined,
|
||||
): {
|
||||
isOverSize: boolean;
|
||||
// 单位 MB
|
||||
maxSize?: number;
|
||||
} => {
|
||||
if (!file) {
|
||||
return {
|
||||
isOverSize: false,
|
||||
};
|
||||
}
|
||||
if (!config) {
|
||||
return {
|
||||
isOverSize: false,
|
||||
};
|
||||
}
|
||||
const inputType = getInputTypeFromFile(file);
|
||||
if (!inputType) {
|
||||
return {
|
||||
isOverSize: false,
|
||||
};
|
||||
}
|
||||
const { maxSize } = config[inputType];
|
||||
if (!maxSize) {
|
||||
return {
|
||||
isOverSize: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isOverSize: file.size > maxSize * 1024,
|
||||
maxSize,
|
||||
};
|
||||
};
|
||||
|
||||
// 根据acceptUploadItemTypes获取accept
|
||||
export const getAcceptByUploadItemTypes = (
|
||||
acceptUploadItemTypes: UploadItemType[],
|
||||
) => {
|
||||
const accept: string[] = [];
|
||||
for (const type of acceptUploadItemTypes) {
|
||||
if (!type) {
|
||||
continue;
|
||||
}
|
||||
const acceptStr = acceptMap[type];
|
||||
if (!acceptStr) {
|
||||
continue;
|
||||
}
|
||||
accept.push(...acceptStr.split(','));
|
||||
}
|
||||
return accept.join(',');
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import { Select, withField } from '@coze-arch/bot-semi';
|
||||
import { InputType } from '@coze-arch/bot-api/playground_api';
|
||||
type InputWithInputTypeProps = {
|
||||
value?: { type: InputType; value: string };
|
||||
onSelect?: (value: { type: InputType; value: string }) => void;
|
||||
} & Omit<React.ComponentProps<typeof Select>, 'value' | 'onSelect'>;
|
||||
|
||||
const SelectWithInputType: FC<InputWithInputTypeProps> = props => {
|
||||
const { value, onSelect, ...rest } = props;
|
||||
return (
|
||||
<Select
|
||||
{...rest}
|
||||
showClear={!!value?.value}
|
||||
onClear={() => {
|
||||
onSelect?.({ type: InputType.TextInput, value: '' });
|
||||
}}
|
||||
value={value?.value}
|
||||
onSelect={selectValue => {
|
||||
const newValue = {
|
||||
type: value?.type || InputType.TextInput,
|
||||
value: selectValue as string,
|
||||
};
|
||||
onSelect?.(newValue);
|
||||
return newValue;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectWithInputTypeField: FC<
|
||||
InputWithInputTypeProps & CommonFieldProps
|
||||
> = withField(SelectWithInputType, {
|
||||
valueKey: 'value',
|
||||
onKeyChangeFnName: 'onSelect',
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UIInput } from '@coze-arch/bot-semi';
|
||||
|
||||
export const UploadField = () => <UIInput disabled />;
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
import { type ToolInfo } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export interface ComponentParameterProps {
|
||||
toolInfo: ToolInfo;
|
||||
parameter: string;
|
||||
}
|
||||
export const ComponentParameter: FC<ComponentParameterProps> = ({
|
||||
toolInfo,
|
||||
parameter,
|
||||
}) => {
|
||||
const { tool_params_list = [] } = toolInfo;
|
||||
const { name, type, required, desc } =
|
||||
tool_params_list.find(item => item.name === parameter) || {};
|
||||
return (
|
||||
<div className="px-2 flex items-center justify-center coz-fg-secondary max-w-[86px]">
|
||||
<Text className="mr-1" ellipsis>
|
||||
{name}
|
||||
</Text>
|
||||
<Tooltip
|
||||
className="max-w-[226px]"
|
||||
content={
|
||||
<div className="flex flex-col justify-center" key={name}>
|
||||
<div className="flex items-center">
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: name || '',
|
||||
position: 'top',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-medium mr-[9px]">
|
||||
{name || '-'}
|
||||
</span>
|
||||
</Text>
|
||||
<span className="rounded coz-mg-primary px-[6px] py-[1px] mr-[3px]">
|
||||
{type}
|
||||
</span>
|
||||
{Boolean(required) && (
|
||||
<span className="rounded coz-mg-primary px-[6px] py-[1px]">
|
||||
{I18n.t('workflow_add_parameter_required')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="mt-[3px] coze-fg-primary text-sm">
|
||||
{desc || '-'}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconInfo />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
.upload-content {
|
||||
:global {
|
||||
.semi-checkbox-inner {
|
||||
.semi-checkbox-inner-display {
|
||||
}
|
||||
}
|
||||
|
||||
.semi-checkbox-content {
|
||||
flex: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { Toast, UIInput, Popover, Form } from '@coze-arch/bot-semi';
|
||||
import { IconChevronDown } from '@douyinfe/semi-icons';
|
||||
|
||||
import {
|
||||
type ComponentTypeItem,
|
||||
type ComponentTypeSelectContentRadioValueType,
|
||||
type SelectComponentTypeItem,
|
||||
type UploadComponentTypeItem,
|
||||
} from '../types';
|
||||
import { UploadContent } from './upload-contnet';
|
||||
import { SelectContentField } from './select-content';
|
||||
import { formatComponentTypeForm } from './method';
|
||||
|
||||
const { RadioGroup, Radio } = Form;
|
||||
|
||||
const SelectTypeAndLableMap: Record<
|
||||
ComponentTypeSelectContentRadioValueType,
|
||||
string
|
||||
> = {
|
||||
text: I18n.t('shortcut_component_type_text'),
|
||||
select: I18n.t('shortcut_component_type_selector'),
|
||||
upload: I18n.t('shortcut_modal_components_modal_upload_component'),
|
||||
};
|
||||
|
||||
export const ComponentTypeSelectRecordItem = (props: {
|
||||
value: ComponentTypeItem;
|
||||
onSubmit?: (value: ComponentTypeItem) => void;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { value: defaultValue, onSubmit, disabled = false } = props;
|
||||
const [submitValue, setSubmitValue] =
|
||||
useState<ComponentTypeItem>(defaultValue);
|
||||
const [componentType, setComponentType] =
|
||||
useState<ComponentTypeItem>(defaultValue);
|
||||
const [selectPopoverVisible, setSelectPopoverVisible] = useState(false);
|
||||
const componentTypeSelectFormRef = useRef<{
|
||||
formApi: ComponentTypeSelectFormMethods;
|
||||
} | null>(null);
|
||||
const onComponentTypeSelectFormSubmit = async () => {
|
||||
if (await componentTypeSelectFormRef.current?.formApi.validate()) {
|
||||
if (!componentType) {
|
||||
return;
|
||||
}
|
||||
onSubmit?.(componentType);
|
||||
setSubmitValue(componentType);
|
||||
setSelectPopoverVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSubmitValue(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<Popover
|
||||
trigger="custom"
|
||||
footer={null}
|
||||
visible={selectPopoverVisible}
|
||||
position="topRight"
|
||||
onClickOutSide={() => setSelectPopoverVisible(false)}
|
||||
content={() => (
|
||||
<div className="p-6 w-[288px]">
|
||||
<ComponentTypeSelectForm
|
||||
ref={componentTypeSelectFormRef}
|
||||
value={submitValue}
|
||||
onChange={setComponentType}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
color="highlight"
|
||||
onClick={() => setSelectPopoverVisible(false)}
|
||||
>
|
||||
{I18n.t('cancel')}
|
||||
</Button>
|
||||
<Button onClick={onComponentTypeSelectFormSubmit}>
|
||||
{I18n.t('Confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<UIInput
|
||||
className={cls('w-full', disabled && '!pointer-events-auto')}
|
||||
suffix={
|
||||
<IconChevronDown
|
||||
onClick={() => setSelectPopoverVisible(!selectPopoverVisible)}
|
||||
/>
|
||||
}
|
||||
placeholder={I18n.t(
|
||||
'shortcut_modal_selector_component_default_text',
|
||||
)}
|
||||
value={SelectTypeAndLableMap[submitValue?.type]}
|
||||
onClick={() => setSelectPopoverVisible(!selectPopoverVisible)}
|
||||
disabled={disabled}
|
||||
readonly
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ComponentTypeSelectFormProps {
|
||||
value: ComponentTypeItem;
|
||||
onChange?: (values: ComponentTypeItem) => void;
|
||||
}
|
||||
|
||||
export interface ComponentTypeSelectFormMethods {
|
||||
validate: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const ComponentTypeSelectForm = forwardRef<
|
||||
{ formApi?: ComponentTypeSelectFormMethods },
|
||||
ComponentTypeSelectFormProps
|
||||
>((props, ref) => {
|
||||
const { value, onChange } = props;
|
||||
const [selectOption, setSelectOption] =
|
||||
useState<ComponentTypeSelectContentRadioValueType>(value.type);
|
||||
const optionsMap = getComponentTypeOptionMap(value);
|
||||
const formRef = useRef<Form>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
formApi: {
|
||||
validate: async () => {
|
||||
try {
|
||||
if (selectOption === 'select') {
|
||||
return Boolean(
|
||||
await formRef.current?.formApi.validate(['values.options']),
|
||||
);
|
||||
}
|
||||
if (selectOption === 'upload') {
|
||||
return Boolean(
|
||||
await formRef.current?.formApi.validate(['values.uploadTypes']),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (errors: any) {
|
||||
if (selectOption === 'select') {
|
||||
const message = errors?.values?.options;
|
||||
message && Toast.error(message);
|
||||
}
|
||||
if (selectOption === 'upload') {
|
||||
const message = errors?.values?.uploadTypes;
|
||||
message && Toast.error(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form<{ values: ComponentTypeItem }>
|
||||
autoComplete="off"
|
||||
ref={formRef}
|
||||
initValues={{ values: value }}
|
||||
className="flex flex-col gap-6"
|
||||
onValueChange={({ values }) => {
|
||||
onChange?.(formatComponentTypeForm(values));
|
||||
}}
|
||||
>
|
||||
<div className="coz-fg-plus text-[16px] font-medium">
|
||||
{I18n.t('shortcut_modal_components_modal_component_type')}
|
||||
</div>
|
||||
<RadioGroup
|
||||
fieldStyle={{
|
||||
padding: 0,
|
||||
}}
|
||||
className="flex flex-col !p-0 gap-3"
|
||||
defaultValue={selectOption}
|
||||
field="values.type"
|
||||
noLabel
|
||||
onChange={e => {
|
||||
setSelectOption(e.target.value);
|
||||
}}
|
||||
>
|
||||
{Object.entries(optionsMap).map(([key, { label }]) => (
|
||||
<Radio value={key}>{label}</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{Object.entries(optionsMap).map(([key, { render }]) => (
|
||||
<div
|
||||
key={key}
|
||||
className={cls({
|
||||
hidden: key !== selectOption,
|
||||
})}
|
||||
>
|
||||
{render?.()}
|
||||
</div>
|
||||
))}
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
const getComponentTypeOptionMap = (
|
||||
initValue?: ComponentTypeItem,
|
||||
): {
|
||||
[key in ComponentTypeSelectContentRadioValueType]: {
|
||||
label: string;
|
||||
render?: () => React.ReactNode;
|
||||
};
|
||||
} => ({
|
||||
text: {
|
||||
label: I18n.t('shortcut_component_type_text'),
|
||||
},
|
||||
select: {
|
||||
label: I18n.t('shortcut_component_type_selector'),
|
||||
render: () => (
|
||||
<SelectContentField
|
||||
field="values.options"
|
||||
value={(initValue as SelectComponentTypeItem)?.options}
|
||||
/>
|
||||
),
|
||||
},
|
||||
upload: {
|
||||
label: I18n.t('shortcut_modal_components_modal_upload_component'),
|
||||
render: () => (
|
||||
<UploadContent
|
||||
value={(initValue as UploadComponentTypeItem)?.uploadTypes}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ComponentTypeItem } from '../types';
|
||||
|
||||
export const formatComponentTypeForm = (
|
||||
values: ComponentTypeItem,
|
||||
): ComponentTypeItem => {
|
||||
const { type } = values;
|
||||
if (type === 'text') {
|
||||
return { type };
|
||||
}
|
||||
if (type === 'select') {
|
||||
return { type, options: values.options };
|
||||
}
|
||||
if (type === 'upload') {
|
||||
return { type, uploadTypes: values.uploadTypes };
|
||||
}
|
||||
return values;
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type FC,
|
||||
} from 'react';
|
||||
|
||||
import { SortableList } from '@coze-studio/components/sortable-list';
|
||||
import { type TItemRender } from '@coze-studio/components';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
UIButton,
|
||||
UIInput,
|
||||
useFieldApi,
|
||||
useFieldState,
|
||||
withField,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconAdd,
|
||||
IconShortcutDisorder,
|
||||
IconShortcutTrash,
|
||||
} from '@coze-arch/bot-icons';
|
||||
|
||||
import { shortid } from '../../../../utils/uuid';
|
||||
|
||||
export interface OptionData {
|
||||
value?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface OptionListProps {
|
||||
options: OptionData[];
|
||||
onChange: Dispatch<SetStateAction<OptionData[]>>;
|
||||
}
|
||||
|
||||
const dndType = Symbol.for(
|
||||
'chat-area-plugins-chat-shortcuts-component-options-dnd-list',
|
||||
);
|
||||
|
||||
export const OptionsList: FC<OptionListProps> = ({ options, onChange }) => {
|
||||
const sortable = options.length > 1;
|
||||
const itemRender = useMemo<TItemRender<OptionData>>(
|
||||
() =>
|
||||
({ data, connect }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const dropRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const handleRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
connect(dropRef, handleRef);
|
||||
return (
|
||||
<div ref={dropRef} className="flex items-center mb-6 last:mb-0">
|
||||
<UIInput
|
||||
className="flex-1"
|
||||
value={data.value}
|
||||
maxLength={20}
|
||||
onChange={value => {
|
||||
onChange(_options => {
|
||||
const index = _options.findIndex(item => item.id === data.id);
|
||||
_options.splice(index, 1, {
|
||||
value,
|
||||
id: data.id,
|
||||
});
|
||||
return [..._options];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="ml-2" ref={handleRef}>
|
||||
<IconButton
|
||||
size="small"
|
||||
className={sortable ? 'cursor-grab' : ''}
|
||||
icon={<IconShortcutDisorder />}
|
||||
disabled={!sortable}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<Tooltip content={I18n.t('Remove')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
icon={<IconShortcutTrash />}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
disabled={options.length <= 1}
|
||||
onClick={() => {
|
||||
onChange(_options =>
|
||||
_options.filter(item => item.id !== data.id),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[sortable],
|
||||
);
|
||||
return (
|
||||
<SortableList
|
||||
type={dndType}
|
||||
list={options}
|
||||
itemRender={itemRender}
|
||||
onChange={onChange}
|
||||
enabled={sortable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MAX_OPTIONS = 20;
|
||||
|
||||
export interface SelectContentProps {
|
||||
value?: string[];
|
||||
onChange?: (newOptions: string[]) => void;
|
||||
hasError?: boolean;
|
||||
}
|
||||
export const SelectContent: FC<SelectContentProps> = ({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
hasError,
|
||||
}) => {
|
||||
const [options, setOptions] = useState<OptionData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(
|
||||
(initialValue?.length ? initialValue : ['']).map<OptionData>(item => ({
|
||||
value: item,
|
||||
id: shortid(),
|
||||
})),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const values = options
|
||||
.map(option => option.value?.trim())
|
||||
.filter(value => !!value);
|
||||
onChange?.(values as string[]);
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="coz-fg-plus mb-[14px] font-medium">
|
||||
{I18n.t('shortcut_modal_selector_component_options')}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<UIButton
|
||||
size="small"
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
disabled={options.length >= MAX_OPTIONS}
|
||||
icon={<IconAdd />}
|
||||
className="!coz-fg-hglt text-sm font-medium"
|
||||
onClick={() => {
|
||||
setOptions([
|
||||
...options,
|
||||
{
|
||||
value: '',
|
||||
id: shortid(),
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{I18n.t('shortcut_modal_selector_component_options')}
|
||||
</UIButton>
|
||||
</div>
|
||||
<div className="max-h-40 my-6 overflow-y-auto">
|
||||
<OptionsList options={options} onChange={setOptions} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectContentFieldInner = withField(SelectContent);
|
||||
|
||||
export const SelectContentField: FC<
|
||||
CommonFieldProps & SelectContentProps
|
||||
> = props => {
|
||||
const state = useFieldState(props.field);
|
||||
const api = useFieldApi(props.field);
|
||||
return (
|
||||
<div onMouseEnter={() => api.setError('')}>
|
||||
<SelectContentFieldInner
|
||||
{...props}
|
||||
pure
|
||||
hasError={!!state.error?.length}
|
||||
trigger="custom"
|
||||
rules={[
|
||||
{
|
||||
validator: (rules, value) => !!value?.length,
|
||||
message: I18n.t(
|
||||
'shortcut_modal_selector_component_no_options_error',
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
import { InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type UploadComponentTypeItem } from '../types';
|
||||
import { ACCEPT_UPLOAD_TYPES } from '../../../../utils/file-const';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface UploadContentProps {
|
||||
value: UploadComponentTypeItem['uploadTypes'] | undefined;
|
||||
onChange?: (value: UploadComponentTypeItem['uploadTypes']) => void;
|
||||
}
|
||||
|
||||
const { Checkbox, CheckboxGroup } = Form;
|
||||
|
||||
const DefaultValue = [
|
||||
InputType.UploadImage,
|
||||
InputType.UploadAudio,
|
||||
InputType.UploadDoc,
|
||||
InputType.UploadTable,
|
||||
InputType.CODE,
|
||||
InputType.ARCHIVE,
|
||||
InputType.PPT,
|
||||
InputType.VIDEO,
|
||||
InputType.TXT,
|
||||
];
|
||||
|
||||
export const UploadContent = (props: UploadContentProps) => {
|
||||
const { value = DefaultValue, onChange } = props;
|
||||
return (
|
||||
<>
|
||||
<div className="coz-fg-plus text-[16px] font-medium">
|
||||
{I18n.t('shortcut_modal_upload_component_supported_file_formats')}
|
||||
</div>
|
||||
<CheckboxGroup
|
||||
field="values.uploadTypes"
|
||||
onChange={checkedValues => {
|
||||
onChange?.(checkedValues);
|
||||
}}
|
||||
initValue={value}
|
||||
className={cls('flex flex-wrap flex-row', styles['upload-content'])}
|
||||
noLabel
|
||||
noErrorMessage
|
||||
rules={[
|
||||
{
|
||||
validator: (rules, newValue) => !!newValue?.length,
|
||||
message: I18n.t(
|
||||
'shortcut_modal_please_select_file_formats_for_upload_component_tip',
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{ACCEPT_UPLOAD_TYPES.map(({ type, label, icon }) => (
|
||||
<div key={type} className="flex-1 basis-1/2">
|
||||
<Checkbox
|
||||
className="flex-row-reverse justify-end"
|
||||
noLabel
|
||||
defaultChecked={value?.includes(type)}
|
||||
value={type}
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
<img src={icon} alt={label} className="w-5 h-[25px] mr-2" />
|
||||
{label}
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</CheckboxGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const tableEmpty = (useTool: boolean, selected: boolean) => (
|
||||
<div className={styles.empty}>
|
||||
{useTool
|
||||
? selected
|
||||
? I18n.t('shortcut_modal_skill_has_no_param_tip')
|
||||
: I18n.t('shortcut_modal_skill_select_button')
|
||||
: I18n.t('shortcut_modal_form_to_be_filled_up_tip')}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
.table {
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
:global {
|
||||
.semi-table-placeholder {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.semi-form-field {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.semi-table-tbody {
|
||||
tr:first-child td {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--coz-fg-dim);
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { DndProvider } from '@coze-studio/components/dnd-provider';
|
||||
import { type OnMove } from '@coze-studio/components';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type FormApi } from '@coze-arch/bot-semi/Form';
|
||||
import { Form, Table, Toast, Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
import {
|
||||
InputType,
|
||||
ToolType,
|
||||
type ToolInfo,
|
||||
type shortcut_command,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { compTip } from '../components/tip';
|
||||
import FieldLabel from '../components/field-label';
|
||||
import ActionButton from '../components/action-button';
|
||||
import { shortid } from '../../../utils/uuid';
|
||||
import { type ComponentsWithId } from './types';
|
||||
import { getColumns, tableComponents } from './table-components';
|
||||
import {
|
||||
attachIdToComponents,
|
||||
checkDuplicateName,
|
||||
formatSubmitValues,
|
||||
} from './method';
|
||||
import { tableEmpty } from './empty';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ComponentsTableProps {
|
||||
components: shortcut_command.Components[]; // 变量列表
|
||||
onChange?: (components: shortcut_command.Components[]) => void;
|
||||
toolType?: shortcut_command.ToolType;
|
||||
toolInfo: ToolInfo;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentsTableActions {
|
||||
validate: () => Promise<shortcut_command.Components[]>;
|
||||
setValues: (values: shortcut_command.Components[]) => void;
|
||||
}
|
||||
|
||||
const MAX_COMPONENTS = 10;
|
||||
|
||||
// 半受控组件,使用初始值 + 暴露 API 的方式,方便内部维护本地 id 用于拖拽排序标识数据
|
||||
export const ComponentsTable = forwardRef<
|
||||
{ formApi?: ComponentsTableActions },
|
||||
ComponentsTableProps
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
>(({ components, onChange, toolType, disabled, toolInfo }, ref) => {
|
||||
const [values, setValues] = useState<ComponentsWithId[]>(
|
||||
attachIdToComponents(components),
|
||||
);
|
||||
const formRef = useRef<FormApi<{ values: ComponentsWithId[] }>>();
|
||||
|
||||
const onChangeInner = (newValues: ComponentsWithId[]) => {
|
||||
setValues(newValues);
|
||||
formRef.current?.setValues({ values: newValues }, { isOverride: true });
|
||||
onChange?.(formatSubmitValues(newValues));
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
formApi: formRef.current
|
||||
? {
|
||||
validate: async (...props) => {
|
||||
// 在这里统一处理,避免多个相同字段触发多次 toast
|
||||
if (
|
||||
values.some(
|
||||
component =>
|
||||
component.input_type === InputType.Select &&
|
||||
!component.options?.length,
|
||||
)
|
||||
) {
|
||||
Toast.error(
|
||||
I18n.t('shortcut_modal_selector_component_no_options_error'),
|
||||
);
|
||||
throw Error('shortcut_modal_selector_component_no_options_error');
|
||||
}
|
||||
if (
|
||||
formRef.current &&
|
||||
checkDuplicateName(values, formRef.current)
|
||||
) {
|
||||
throw Error('duplicated names');
|
||||
}
|
||||
|
||||
const submitValues = await formRef.current?.validate(...props);
|
||||
return formatSubmitValues(submitValues?.values ?? []);
|
||||
},
|
||||
setValues: newComponents => {
|
||||
const newValues = attachIdToComponents(newComponents);
|
||||
setValues(newValues);
|
||||
formRef.current?.setValues(
|
||||
{ values: newValues },
|
||||
{ isOverride: true },
|
||||
);
|
||||
formRef.current?.setTouched('values', false);
|
||||
formRef.current?.setError('values', '');
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const onMove: OnMove<string> = (sourceId, targetId, isBefore) => {
|
||||
const newValues = [...values];
|
||||
const sourceIndex = newValues.findIndex(source => source.id === sourceId);
|
||||
const errors = formRef.current?.getError('values') || [];
|
||||
const sourceError = errors.splice(sourceIndex, 1)[0];
|
||||
const sourceItem = newValues.splice(sourceIndex, 1)[0];
|
||||
const targetIndex =
|
||||
newValues.findIndex(target => target.id === targetId) +
|
||||
(isBefore ? 0 : 1);
|
||||
sourceItem && newValues.splice(targetIndex, 0, sourceItem);
|
||||
errors.splice(targetIndex, 0, sourceError);
|
||||
// 前后 index 相同的情况不触发 onChange 避免频繁 rerender
|
||||
if (sourceIndex !== targetIndex) {
|
||||
onChangeInner(newValues);
|
||||
// 只在拖拽排序后,需要手动更新 form value
|
||||
formRef.current?.setValues(
|
||||
{
|
||||
values: newValues,
|
||||
},
|
||||
{ isOverride: true },
|
||||
);
|
||||
formRef.current?.setError('values', errors);
|
||||
}
|
||||
};
|
||||
|
||||
const showAdd =
|
||||
toolType === undefined ||
|
||||
![ToolType.ToolTypeWorkFlow, ToolType.ToolTypePlugin].includes(toolType);
|
||||
|
||||
const selected = !!toolInfo?.tool_name;
|
||||
const oversize = values.length >= MAX_COMPONENTS;
|
||||
|
||||
const addBtn = (
|
||||
<ActionButton
|
||||
icon={<IconAdd />}
|
||||
disabled={oversize || disabled}
|
||||
onClick={() => {
|
||||
onChangeInner([
|
||||
...values,
|
||||
{
|
||||
id: shortid(),
|
||||
input_type: InputType.TextInput,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{I18n.t('add')}
|
||||
</ActionButton>
|
||||
);
|
||||
|
||||
const tipBtn = oversize ? (
|
||||
<Tooltip
|
||||
content={I18n.t('shortcut_modal_max_component_tip', {
|
||||
maxCount: MAX_COMPONENTS,
|
||||
})}
|
||||
>
|
||||
{addBtn}
|
||||
</Tooltip>
|
||||
) : (
|
||||
addBtn
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pb-6">
|
||||
<div className="flex items-center justify-between pb-1.5">
|
||||
<FieldLabel tip={compTip()}>
|
||||
{I18n.t('shortcut_modal_components')}
|
||||
</FieldLabel>
|
||||
{showAdd ? tipBtn : null}
|
||||
</div>
|
||||
<DndProvider>
|
||||
<Form<{ values: ComponentsWithId[] }>
|
||||
initValues={{ values }}
|
||||
// 手动触发校验,避免受增删和拖拽排序影响
|
||||
trigger="custom"
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
getFormApi={api => (formRef.current = api)}
|
||||
onValueChange={(newValues, changedValues) => {
|
||||
const changedKeys = Object.keys(changedValues);
|
||||
if (
|
||||
changedKeys.length === 1 &&
|
||||
// 只在表单修改场景下触发 onChange 避免无限循环
|
||||
changedKeys[0]?.startsWith('values.[')
|
||||
) {
|
||||
onChangeInner([...newValues.values]);
|
||||
// 只在编辑表单场景下对具体字段触发校验,其它场景(整行的增删排序)不触发校验
|
||||
setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
checkDuplicateName(newValues.values, formRef.current);
|
||||
}
|
||||
// @ts-expect-error semi 的类型定义无法支持多段 path
|
||||
formRef.current?.validate([changedKeys[0]]);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.table}>
|
||||
<Table<ComponentsWithId>
|
||||
dataSource={values}
|
||||
size="small"
|
||||
columns={getColumns({
|
||||
components: values,
|
||||
onChange: onChangeInner,
|
||||
toolInfo,
|
||||
toolType,
|
||||
disabled,
|
||||
})}
|
||||
components={tableComponents}
|
||||
pagination={false}
|
||||
onRow={item => ({
|
||||
id: item?.id ?? '',
|
||||
sortable: (values?.length ?? 0) > 1 && !disabled,
|
||||
onMove,
|
||||
})}
|
||||
empty={tableEmpty(!showAdd, selected)}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</DndProvider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FormApi } from '@coze-arch/bot-semi/Form';
|
||||
import {
|
||||
InputType,
|
||||
type shortcut_command,
|
||||
type ToolInfo,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { shortid } from '../../../utils/uuid';
|
||||
import { type UploadItemType } from '../../../utils/file-const';
|
||||
import { type ComponentsWithId, type ComponentTypeItem } from './types';
|
||||
|
||||
const MAX_COMPONENTS = 10;
|
||||
|
||||
export const attachIdToComponents = (
|
||||
components: shortcut_command.Components[],
|
||||
): ComponentsWithId[] =>
|
||||
components.map(item => ({
|
||||
...item,
|
||||
id: shortid(),
|
||||
}));
|
||||
|
||||
export const formatSubmitValues = (
|
||||
values: ComponentsWithId[],
|
||||
): shortcut_command.Components[] =>
|
||||
values.map(({ id, options, ...value }) => ({
|
||||
...value,
|
||||
options: value.input_type === InputType.Select ? options : [],
|
||||
}));
|
||||
|
||||
export const checkDuplicateName = (
|
||||
values: ComponentsWithId[],
|
||||
formApi: FormApi,
|
||||
) => {
|
||||
const fieldMap: Record<string, number[]> = {};
|
||||
values.forEach((item, index) => {
|
||||
if (item.name) {
|
||||
if (fieldMap[item.name]) {
|
||||
fieldMap[item.name]?.push(index);
|
||||
} else {
|
||||
fieldMap[item.name] = [index];
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
// 避免修改后立刻被 field 自己的校验状态覆盖
|
||||
Object.entries(fieldMap).forEach(([name, indexArray]) => {
|
||||
const isDuplicated = indexArray.length > 1;
|
||||
indexArray.forEach(index => {
|
||||
formApi.setError(`values.${index}.name`, !isDuplicated);
|
||||
});
|
||||
});
|
||||
});
|
||||
return Object.entries(fieldMap).some(
|
||||
([name, indexArr]) => indexArr.length > 1,
|
||||
);
|
||||
};
|
||||
|
||||
export interface SubmitComponentTypeFields {
|
||||
input_type?: InputType;
|
||||
options?: string[];
|
||||
upload_options?: UploadItemType[];
|
||||
}
|
||||
|
||||
export const getComponentTypeSelectFormInitValues = (): ComponentTypeItem => ({
|
||||
type: 'text',
|
||||
});
|
||||
|
||||
// 定义一个映射对象,将ComponentTypeItem的type映射到对应的input_type和其他字段
|
||||
const componentTypeHandlers = {
|
||||
text: () => ({ input_type: InputType.TextInput }),
|
||||
select: (value: ComponentTypeItem) => {
|
||||
const { type } = value;
|
||||
if (type !== 'select') {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
input_type: InputType.Select,
|
||||
options: value.options,
|
||||
};
|
||||
},
|
||||
upload: (value: ComponentTypeItem) => {
|
||||
if (value.type !== 'upload') {
|
||||
return;
|
||||
}
|
||||
const { uploadTypes } = value;
|
||||
|
||||
if (uploadTypes.length > 1) {
|
||||
return {
|
||||
input_type: InputType.MixUpload,
|
||||
upload_options: uploadTypes,
|
||||
};
|
||||
}
|
||||
return {
|
||||
input_type: uploadTypes.at(0) as InputType,
|
||||
upload_options: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const getSubmitFieldFromComponentTypeForm = (
|
||||
values: ComponentTypeItem,
|
||||
): SubmitComponentTypeFields => {
|
||||
const { type } = values;
|
||||
|
||||
const handler = componentTypeHandlers[type];
|
||||
|
||||
const result = handler && handler(values);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果没有找到处理函数,就返回默认值
|
||||
return { input_type: InputType.TextInput };
|
||||
};
|
||||
|
||||
// 是否是上传类型
|
||||
export const isUploadType = (
|
||||
type: InputType,
|
||||
): type is
|
||||
| InputType.UploadImage
|
||||
| InputType.UploadDoc
|
||||
| InputType.UploadTable
|
||||
| InputType.UploadAudio
|
||||
| InputType.CODE
|
||||
| InputType.ARCHIVE
|
||||
| InputType.PPT
|
||||
| InputType.VIDEO
|
||||
| InputType.TXT
|
||||
| InputType.MixUpload =>
|
||||
[
|
||||
InputType.UploadImage,
|
||||
InputType.UploadDoc,
|
||||
InputType.UploadTable,
|
||||
InputType.UploadAudio,
|
||||
InputType.CODE,
|
||||
InputType.ARCHIVE,
|
||||
InputType.PPT,
|
||||
InputType.VIDEO,
|
||||
InputType.TXT,
|
||||
InputType.MixUpload,
|
||||
].includes(type);
|
||||
|
||||
// 将input_type映射到对应的处理函数
|
||||
const inputTypeHandlers = {
|
||||
[InputType.TextInput]: () => ({ type: 'text' }),
|
||||
[InputType.Select]: (options: string[] = []) => ({
|
||||
type: 'select' as const,
|
||||
options,
|
||||
}),
|
||||
upload: (uploadTypes: UploadItemType[] = []) => ({
|
||||
type: 'upload' as const,
|
||||
uploadTypes,
|
||||
}),
|
||||
};
|
||||
|
||||
export const getComponentTypeFormBySubmitField = (
|
||||
values: SubmitComponentTypeFields,
|
||||
): ComponentTypeItem => {
|
||||
const { input_type, options, upload_options } = values;
|
||||
|
||||
if (!input_type) {
|
||||
return getComponentTypeSelectFormInitValues();
|
||||
}
|
||||
|
||||
if (isUploadType(input_type)) {
|
||||
const handler = inputTypeHandlers.upload;
|
||||
return handler(upload_options);
|
||||
}
|
||||
|
||||
const handler = inputTypeHandlers[input_type];
|
||||
|
||||
if (handler) {
|
||||
return handler(options);
|
||||
}
|
||||
|
||||
return getComponentTypeSelectFormInitValues();
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. 修改components列表中对应组件的hide:true
|
||||
*/
|
||||
export const modifyComponentWhenSwitchChange = ({
|
||||
components,
|
||||
record,
|
||||
checked,
|
||||
}: {
|
||||
components: ComponentsWithId[];
|
||||
record: ComponentsWithId;
|
||||
checked: boolean;
|
||||
}) =>
|
||||
components.map(item => {
|
||||
if (item.id === record.id) {
|
||||
return {
|
||||
...item,
|
||||
hide: !checked,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
// components switch是否disable
|
||||
export const isSwitchDisabled = ({
|
||||
components,
|
||||
record,
|
||||
toolInfo,
|
||||
}: {
|
||||
components: ComponentsWithId[];
|
||||
record: ComponentsWithId;
|
||||
toolInfo: ToolInfo;
|
||||
}) => {
|
||||
const { default_value } = record ?? {};
|
||||
const isWithDefaultValue = !!default_value?.value;
|
||||
const isRequired = (() => {
|
||||
if (!toolInfo?.tool_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用工具&为必填参数
|
||||
*/
|
||||
return !!toolInfo?.tool_params_list?.find(t => t.name === record.parameter)
|
||||
?.required;
|
||||
})();
|
||||
|
||||
// 组件超过最大数量, 不允许开启
|
||||
const isMaxCount =
|
||||
record.hide && components.filter(com => !com.hide).length >= MAX_COMPONENTS;
|
||||
|
||||
/** 必填且没有默认值不允许关闭 */
|
||||
const isFinalRequired = isRequired && !isWithDefaultValue;
|
||||
|
||||
return isFinalRequired || isMaxCount;
|
||||
};
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { useDnDSortableItem } from '@coze-studio/components/sortable-list-hooks';
|
||||
import { type OnMove } from '@coze-studio/components';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Switch } from '@coze-arch/coze-design';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import {
|
||||
type ColumnProps,
|
||||
type TableComponents,
|
||||
} from '@coze-arch/bot-semi/Table';
|
||||
import { Form, IconButton, Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconShortcutTrash, IconSvgShortcutDrag } from '@coze-arch/bot-icons';
|
||||
import {
|
||||
shortcut_command,
|
||||
type ToolInfo,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type UploadItemType } from '../../../utils/file-const';
|
||||
import { type ComponentsWithId } from './types';
|
||||
import {
|
||||
getComponentTypeFormBySubmitField,
|
||||
getSubmitFieldFromComponentTypeForm,
|
||||
isSwitchDisabled,
|
||||
modifyComponentWhenSwitchChange,
|
||||
} from './method';
|
||||
import { ComponentTypeSelectRecordItem } from './component-type-select';
|
||||
import { ComponentParameter } from './component-parameter';
|
||||
import { ComponentDefaultValue } from './component-default-value';
|
||||
|
||||
type ColumnPropType = ColumnProps<ComponentsWithId>;
|
||||
|
||||
const TooltipWithDisabled: FC<TooltipProps & { disabled?: boolean }> = ({
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
}) => (disabled ? <>{children}</> : <Tooltip {...props}>{children}</Tooltip>);
|
||||
|
||||
const getOperationColumns = ({
|
||||
components,
|
||||
onChange,
|
||||
toolType,
|
||||
disabled,
|
||||
toolInfo,
|
||||
}: GetColumnsParams): ColumnPropType => {
|
||||
const deleteable = !disabled;
|
||||
const showDelete = toolType === undefined;
|
||||
|
||||
return {
|
||||
key: 'operation',
|
||||
title: null,
|
||||
width: showDelete ? '80px' : '40px',
|
||||
render: (_, record) => (
|
||||
<div className="flex items-center pl-[12px]">
|
||||
<Switch
|
||||
checked={!record.hide}
|
||||
disabled={isSwitchDisabled({
|
||||
components,
|
||||
record,
|
||||
toolInfo,
|
||||
})}
|
||||
size="mini"
|
||||
onChange={checked =>
|
||||
onChange?.(
|
||||
modifyComponentWhenSwitchChange({
|
||||
components,
|
||||
record,
|
||||
checked,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
{showDelete ? (
|
||||
<div className="px-2">
|
||||
<TooltipWithDisabled
|
||||
content={I18n.t('Remove')}
|
||||
disabled={!deleteable}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
disabled={!deleteable}
|
||||
icon={<IconShortcutTrash />}
|
||||
onClick={() => {
|
||||
onChange?.(components.filter(item => item.id !== record.id));
|
||||
}}
|
||||
/>
|
||||
</TooltipWithDisabled>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const getColumnsMap = (params: GetColumnsParams) => {
|
||||
const { components, disabled } = params;
|
||||
const sortable = components.length > 1 && !disabled;
|
||||
|
||||
return {
|
||||
name: {
|
||||
key: 'name',
|
||||
title: (
|
||||
<Form.Label
|
||||
className="leading-5 p-0 m-0"
|
||||
text={I18n.t('shortcut_modal_component_name')}
|
||||
required
|
||||
/>
|
||||
),
|
||||
width: 1,
|
||||
render: (_, record, index) => (
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
id={handleId}
|
||||
className={cs(
|
||||
'px-[2px]',
|
||||
sortable ? 'cursor-grab' : 'cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<IconSvgShortcutDrag />
|
||||
</div>
|
||||
<Form.Input
|
||||
noLabel
|
||||
maxLength={20}
|
||||
field={`values.[${index}].name`}
|
||||
noErrorMessage
|
||||
placeholder={I18n.t('shortcut_modal_component_name')}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
disabled={disabled || record.hide}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
description: {
|
||||
key: 'description',
|
||||
title: (
|
||||
<Form.Label
|
||||
className="leading-5 p-0 m-0"
|
||||
text={I18n.t('Description')}
|
||||
/>
|
||||
),
|
||||
width: '190px',
|
||||
render: (_, record, index) => (
|
||||
<div className="pl-[2px]">
|
||||
<Form.Input
|
||||
noLabel
|
||||
maxLength={100}
|
||||
field={`values.[${index}].description`}
|
||||
noErrorMessage
|
||||
placeholder={I18n.t('Description')}
|
||||
disabled={disabled || record.hide}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
inputType: {
|
||||
key: 'input_type',
|
||||
title: (
|
||||
<Form.Label
|
||||
className="leading-5 p-0 m-0"
|
||||
text={I18n.t('shortcut_modal_component_type')}
|
||||
required
|
||||
/>
|
||||
),
|
||||
render: (_, record, index) => (
|
||||
<div className="pl-[2px]">
|
||||
<ComponentTypeSelectRecordItem
|
||||
value={getComponentTypeFormBySubmitField({
|
||||
input_type: record.input_type,
|
||||
options: record.options,
|
||||
upload_options: record.upload_options as UploadItemType[],
|
||||
})}
|
||||
disabled={disabled || record.hide}
|
||||
onSubmit={value => {
|
||||
const { input_type, options, upload_options } =
|
||||
getSubmitFieldFromComponentTypeForm(value);
|
||||
params?.onChange?.(
|
||||
params.components.map((item, i) =>
|
||||
i === index
|
||||
? {
|
||||
...item,
|
||||
input_type,
|
||||
options,
|
||||
default_value: {
|
||||
value: '',
|
||||
},
|
||||
upload_options,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
defaultValue: {
|
||||
key: 'default_value',
|
||||
title: (
|
||||
<Form.Label
|
||||
className="leading-5 p-0 m-0"
|
||||
text={I18n.t('shortcut_modal_use_tool_parameter_default_value')}
|
||||
/>
|
||||
),
|
||||
render: (_, record, index) => (
|
||||
<div className="pl-[2px] max-w-[136px]">
|
||||
<ComponentDefaultValue
|
||||
componentType={getComponentTypeFormBySubmitField({
|
||||
input_type: record.input_type,
|
||||
options: record.options,
|
||||
upload_options: record.upload_options as UploadItemType[],
|
||||
})}
|
||||
field={`values.[${index}].default_value`}
|
||||
disabled={disabled || record.hide}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
parameter: {
|
||||
key: 'parameter',
|
||||
title: (
|
||||
<Form.Label
|
||||
className="leading-5 p-0 m-0"
|
||||
text={I18n.t('shortcut_modal_component_plugin_wf_parameter')}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'parameter',
|
||||
render: text => (
|
||||
<ComponentParameter toolInfo={params.toolInfo} parameter={text} />
|
||||
),
|
||||
},
|
||||
operations: getOperationColumns(params),
|
||||
} satisfies Record<string, ColumnPropType>;
|
||||
};
|
||||
|
||||
interface GetColumnsParams {
|
||||
components: ComponentsWithId[];
|
||||
onChange?: (values: ComponentsWithId[]) => void;
|
||||
toolType?: shortcut_command.ToolType;
|
||||
disabled: boolean;
|
||||
toolInfo: ToolInfo;
|
||||
}
|
||||
|
||||
const assignWidth = (base: ColumnPropType, width: string | number) =>
|
||||
Object.assign({}, base, { width });
|
||||
|
||||
export const getColumns = (params: GetColumnsParams): ColumnPropType[] => {
|
||||
const { toolType } = params;
|
||||
const columnsMap = getColumnsMap(params);
|
||||
if (
|
||||
toolType === shortcut_command.ToolType.ToolTypePlugin ||
|
||||
toolType === shortcut_command.ToolType.ToolTypeWorkFlow
|
||||
) {
|
||||
return [
|
||||
assignWidth(columnsMap.name, '103px'),
|
||||
assignWidth(columnsMap.description, '103px'),
|
||||
assignWidth(columnsMap.inputType, '103px'),
|
||||
assignWidth(columnsMap.defaultValue, '126px'),
|
||||
assignWidth(columnsMap.parameter, '86px'),
|
||||
columnsMap.operations,
|
||||
];
|
||||
}
|
||||
return [
|
||||
assignWidth(columnsMap.name, '125px'),
|
||||
assignWidth(columnsMap.description, '125px'),
|
||||
assignWidth(columnsMap.inputType, '125px'),
|
||||
assignWidth(columnsMap.defaultValue, '136px'),
|
||||
columnsMap.operations,
|
||||
];
|
||||
};
|
||||
|
||||
const type = Symbol.for(
|
||||
'chat-area-plugins-chat-shortcuts-components-table-item',
|
||||
);
|
||||
const handleId = 'chat-area-plugins-chat-shortcuts-components-drag-handle';
|
||||
const DraggableBodyRow: FC<
|
||||
PropsWithChildren<{
|
||||
id: string;
|
||||
sortable: boolean;
|
||||
onMove: OnMove<string>;
|
||||
}>
|
||||
> = ({ id, onMove, children, sortable }) => {
|
||||
// 因为 name 可能为空,这里拿 shortid 做一个兜底
|
||||
const dropRef = useRef<HTMLElement>(null);
|
||||
const { connect } = useDnDSortableItem<string>({
|
||||
type,
|
||||
id,
|
||||
onMove,
|
||||
enabled: sortable,
|
||||
});
|
||||
useEffect(() => {
|
||||
// 为了避免复杂的跨组件传值,这里稍微直接操作一下 DOM ,非常抱歉
|
||||
const handleRef = {
|
||||
current: (dropRef.current?.querySelector(`#${handleId}`) ??
|
||||
null) as HTMLElement | null,
|
||||
};
|
||||
connect(dropRef, handleRef);
|
||||
}, []);
|
||||
return <tr ref={dropRef as RefObject<HTMLTableRowElement>}>{children}</tr>;
|
||||
};
|
||||
|
||||
export const tableComponents = {
|
||||
body: {
|
||||
// semi-ui 导出的类型定义非常不负责任
|
||||
row: DraggableBodyRow,
|
||||
},
|
||||
} as unknown as TableComponents;
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type UploadItemType } from '../../../utils/file-const';
|
||||
import { type FileValue } from '../../../components/short-cut-panel/widgets/types';
|
||||
|
||||
export type ComponentsWithId = shortcut_command.Components & { id: string };
|
||||
|
||||
export type ComponentTypeSelectContentRadioValueType =
|
||||
| 'text'
|
||||
| 'select'
|
||||
| 'upload';
|
||||
|
||||
export interface BaseComponentTypeItem {
|
||||
type: ComponentTypeSelectContentRadioValueType;
|
||||
}
|
||||
|
||||
export interface TextComponentTypeItem extends BaseComponentTypeItem {
|
||||
type: 'text';
|
||||
}
|
||||
|
||||
export interface SelectComponentTypeItem extends BaseComponentTypeItem {
|
||||
type: 'select';
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface UploadComponentTypeItem extends BaseComponentTypeItem {
|
||||
type: 'upload';
|
||||
uploadTypes: UploadItemType[];
|
||||
}
|
||||
|
||||
export type ComponentTypeItem =
|
||||
| TextComponentTypeItem
|
||||
| SelectComponentTypeItem
|
||||
| UploadComponentTypeItem;
|
||||
|
||||
export type TValue = string | FileValue | undefined;
|
||||
|
||||
export type TCustomUpload = (uploadParams: {
|
||||
file: File;
|
||||
onProgress?: (percent: number) => void;
|
||||
onSuccess?: (url: string, width?: number, height?: number) => void;
|
||||
onError?: (e: { status?: number }) => void;
|
||||
}) => void;
|
||||
|
||||
export type UploadItemConfig = {
|
||||
[key in UploadItemType]: {
|
||||
maxSize?: number;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
// TODO: hzf, 取名component有点奇怪
|
||||
export type FormInputWithMaxCountProps = {
|
||||
maxCount: number;
|
||||
} & React.ComponentProps<typeof Form.Input>;
|
||||
// input后带上suffix,表示能够输入的最大字数
|
||||
export const FormInputWithMaxCount = (props: FormInputWithMaxCountProps) => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
const handleChange = (v: string) => {
|
||||
setCount(v.length);
|
||||
};
|
||||
const countSuffix = (
|
||||
<div
|
||||
className={style['form-input-with-count']}
|
||||
>{`${count}/${props.maxCount}`}</div>
|
||||
);
|
||||
return (
|
||||
<Form.Input
|
||||
{...props}
|
||||
onChange={value => handleChange(value)}
|
||||
suffix={countSuffix}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
.btn {
|
||||
:global {
|
||||
.semi-icon {
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.semi-button-content-right {
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, type ReactNode, type PropsWithChildren } from 'react';
|
||||
|
||||
import { UIIconButton } from '@coze-arch/bot-semi';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const ActionButton: FC<
|
||||
PropsWithChildren<{
|
||||
icon: ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}>
|
||||
> = ({ onClick, icon, children, disabled }) => (
|
||||
<UIIconButton
|
||||
icon={icon}
|
||||
wrapperClass={styles.btn}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</UIIconButton>
|
||||
);
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC, type PropsWithChildren, type ReactNode } from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { Tooltip, type TooltipProps } from '@coze-arch/coze-design';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
|
||||
export const FieldLabel: FC<
|
||||
PropsWithChildren<{
|
||||
className?: string;
|
||||
tooltip?: TooltipProps;
|
||||
tip?: ReactNode;
|
||||
required?: boolean;
|
||||
}>
|
||||
> = ({ children, className, tooltip, tip, required = false }) => (
|
||||
<div className="flex items-center mb-[6px]">
|
||||
<Form.Label
|
||||
text={children}
|
||||
className="!coz-fg-primary !text-[14px] !leading-[20px] !m-0"
|
||||
required={required}
|
||||
/>
|
||||
{!!tip && (
|
||||
<Tooltip content={tip} {...tooltip}>
|
||||
<IconInfo className={cs('coz-fg-secondary ml-[-12px]', className)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default FieldLabel;
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
const style = {
|
||||
color: 'var(--semi-color-primary-hover)',
|
||||
};
|
||||
|
||||
const getVar = (text: string) => (
|
||||
<span style={style}>
|
||||
{'{{'}
|
||||
{text}
|
||||
{'}}'}
|
||||
</span>
|
||||
);
|
||||
|
||||
const var1 = getVar(
|
||||
I18n.t('shortcut_modal_query_message_hover_tip_component_mode_var1'),
|
||||
);
|
||||
const var2 = getVar(
|
||||
I18n.t('shortcut_modal_query_message_hover_tip_component_mode_var2'),
|
||||
);
|
||||
|
||||
export const queryTip = () => (
|
||||
<div className="p[16px] leading-[16px] text-[12px] font-normal coz-fg-secondary">
|
||||
<h2 className="m-0 mb-[12px] text-[14px] font-medium leading-[20px] coz-fg-plus">
|
||||
{I18n.t('shortcut_modal_query_message_hover_tip_title')}
|
||||
</h2>
|
||||
<ul className="pl-[12px]">
|
||||
<li>
|
||||
{I18n.t('shortcut_modal_query_message_hover_tip_send_query_mode')}
|
||||
</li>
|
||||
<li>
|
||||
{I18n.t('shortcut_modal_query_message_hover_tip_component_mode', {
|
||||
var1,
|
||||
var2,
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<span className="coz-fg-hglt-red w-[12px] inline-block">*</span>
|
||||
{I18n.t(
|
||||
'shortcut_modal_query_message_hover_tip_how_to_insert_components',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const compTip = () =>
|
||||
I18n.t('shortcut_modal_components_hover_tip', {
|
||||
var1,
|
||||
var2,
|
||||
});
|
||||
@@ -0,0 +1,479 @@
|
||||
@ide-tool-prefix: chat-studio-tool-content-block;
|
||||
|
||||
.shortcut-tool-config {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
|
||||
:global {
|
||||
.@{ide-tool-prefix}-content {
|
||||
/* stylelint-disable declaration-no-important */
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.semi-modal-body {
|
||||
padding: 24px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
place-content: center space-between;
|
||||
|
||||
height: 48px;
|
||||
margin-bottom: 4px;
|
||||
padding: 8px;
|
||||
|
||||
background: rgba(6, 7, 9, 8%);
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(6, 7, 9, 16%);
|
||||
border: 1px solid rgba(6, 7, 9, 10%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-item_title {
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.shortcut-item_content {
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgba(6, 7, 9, 50%);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.operation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.operation-item-icon {
|
||||
cursor: pointer;
|
||||
|
||||
:global {
|
||||
.semi-icon {
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-item-icon_hover {
|
||||
&:hover {
|
||||
background: rgba(6, 7, 9, 16%);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-modal {
|
||||
:global {
|
||||
.semi-modal {
|
||||
border-radius: 8px;
|
||||
|
||||
.semi-modal-content {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
.semi-modal-header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.semi-modal-footer {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-common-modal-button-style {
|
||||
min-width: 56px;
|
||||
height: 32px;
|
||||
padding: 6px 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
|
||||
background: rgba(6, 7, 9, 8%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delete-modal-cancel-button {
|
||||
.delete-common-modal-button-style;
|
||||
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
}
|
||||
|
||||
.delete-modal-ok-button {
|
||||
.delete-common-modal-button-style;
|
||||
|
||||
color: #cc1424;
|
||||
background-color: rgba(255, 115, 127, 20%);
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 115, 127, 80%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form-wrapper {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--coz-fg-dim);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.shortcut-action-item {
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.use-tool-checkbox {
|
||||
margin-top: 4px;
|
||||
|
||||
:global {
|
||||
.semi-form-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-modal-body {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-action-radio-group {
|
||||
:global {
|
||||
/* stylelint-disable */
|
||||
.semi-radio-cardRadioGroup .semi-radio-addon,
|
||||
.semi-checkbox-addon {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-form-field-label {
|
||||
margin-bottom: 6px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgba(6, 7, 9, 50%);
|
||||
}
|
||||
|
||||
.semi-input::placeholder,
|
||||
.semi-input-textarea::placeholder {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: rgba(6, 7, 9, 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tool-params-table {
|
||||
padding-bottom: 24px;
|
||||
|
||||
:global {
|
||||
.semi-table-small
|
||||
.semi-table-tbody
|
||||
> .semi-table-row
|
||||
> .semi-table-row-cell {
|
||||
max-width: 200px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.params-value-component_name {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.params-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.params-name-content {
|
||||
.params-name {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: rgba(6, 7, 9, 80%);
|
||||
}
|
||||
|
||||
.params-field {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 6px;
|
||||
background: rgba(6, 7, 9, 4%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgba(6, 7, 9, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-edit-modal {
|
||||
:global {
|
||||
.semi-modal-content {
|
||||
background-color: var(--coz-bg-plus);
|
||||
|
||||
.semi-modal-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.semi-modal-body {
|
||||
padding: 24px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 使 query字段(VarQueryTextarea) 的 popover 能够展示全
|
||||
.semi-modal-wrap,
|
||||
.semi-modal-content,
|
||||
.semi-modal-body {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-modal-wrapper {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
|
||||
box-sizing: content-box;
|
||||
min-height: 0;
|
||||
// 12px + 4px
|
||||
margin: 0 -16px 24px 0;
|
||||
|
||||
> form {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
&.wrapper-border {
|
||||
margin-right: 0;
|
||||
padding-left: 24px;
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
border-radius: 8px;
|
||||
|
||||
> form {
|
||||
padding-top: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-component {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
|
||||
width: 400px;
|
||||
margin-left: 8px;
|
||||
|
||||
background-color: var(--coz-bg-primary);
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
|
||||
animation: ease-in;
|
||||
|
||||
.shortcut-panel {
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-form {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
:global {
|
||||
.semi-input-textarea-counter {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.semi-radioGroup-vertical {
|
||||
row-gap: unset;
|
||||
}
|
||||
|
||||
.semi-radio-cardRadioGroup,
|
||||
.semi-radio-cardRadioGroup_checked,
|
||||
.semi-radio-cardRadioGroup_hover {
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
background: unset;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
background: unset;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-form-field {
|
||||
padding-top: 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-input-with-count {
|
||||
overflow: hidden;
|
||||
|
||||
padding-right: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgba(6, 7, 9, 50%);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-popover-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
width: 320px;
|
||||
padding: 16px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 4px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: rgba(6, 7, 9, 96%);
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 24px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgba(6, 7, 9, 50%);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
min-width: 56px;
|
||||
height: 32px;
|
||||
padding: 6px 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: #fff;
|
||||
|
||||
background: #f22435;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-button-light:not(.semi-button-disabled):hover {
|
||||
background-color: #ba0010;
|
||||
}
|
||||
|
||||
.semi-button-light:not(.semi-button-disabled):active {
|
||||
background-color: #b0000f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-agent-input-wrapper {
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.semi-form-vertical .semi-form-field {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ShortcutEditModalProps, ShortcutEditModal } from './modal';
|
||||
|
||||
export { ShortcutEditModal, ShortcutEditModalProps };
|
||||
|
||||
export const useShortcutEditModal = (
|
||||
props: Omit<ShortcutEditModalProps, 'onClose'>,
|
||||
) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
props.setErrorMessage('');
|
||||
};
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
return {
|
||||
node: visible ? <ShortcutEditModal {...props} onClose={close} /> : null,
|
||||
close,
|
||||
open,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import {
|
||||
type Components,
|
||||
InputType,
|
||||
SendType,
|
||||
type ToolParams,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import type { ShortcutEditFormValues } from '../types';
|
||||
import { initToolEnabledByToolTYpe } from '../../utils/tool-params';
|
||||
import { getDSLFromComponents } from '../../utils/dsl-template';
|
||||
|
||||
export const getSubmitValue = (
|
||||
values: ShortcutEditFormValues,
|
||||
): ShortCutCommand => {
|
||||
const newValues = { ...values };
|
||||
|
||||
/**
|
||||
* 最先执行 根据是否包含 components_list 设置 send_type
|
||||
*/
|
||||
mutableSendType(newValues);
|
||||
|
||||
const { send_type, use_tool = false } = newValues;
|
||||
|
||||
mutableFormatCommandName(newValues);
|
||||
mutableSetCardSchemaForForm(newValues);
|
||||
|
||||
if (send_type === SendType.SendTypeQuery && !use_tool) {
|
||||
mutableInitQueryFormValues(newValues);
|
||||
} else {
|
||||
mutableModifyToolParamsWhenComponentChange(newValues);
|
||||
}
|
||||
|
||||
if (!use_tool) {
|
||||
mutableInitNotUseToolFormValues(newValues);
|
||||
}
|
||||
// TODO: hzf干掉不合理
|
||||
return newValues as ShortCutCommand;
|
||||
};
|
||||
|
||||
const mutableSendType = (value: ShortcutEditFormValues) => {
|
||||
if (value?.components_list?.length) {
|
||||
value.send_type = SendType.SendTypePanel;
|
||||
} else {
|
||||
value.send_type = SendType.SendTypeQuery;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 为了兼容,需要在修改components_list的default_value,hide的时候同步修改toolParams
|
||||
* 1.components_list.hide => !toolParams.refer_component
|
||||
* 2.components_list.default_value => refer_component:false && toolParams.default_value
|
||||
*/
|
||||
const mutableModifyToolParamsWhenComponentChange = (
|
||||
value: ShortcutEditFormValues,
|
||||
): void => {
|
||||
const { components_list, tool_info: { tool_params_list } = {} } = value;
|
||||
if (!components_list || !tool_params_list) {
|
||||
return;
|
||||
}
|
||||
components_list.forEach(com => {
|
||||
const { default_value, hide } = com;
|
||||
const targetToolParams = findToolParamsByComponent(tool_params_list, com);
|
||||
if (!targetToolParams) {
|
||||
return;
|
||||
}
|
||||
targetToolParams.refer_component = !hide;
|
||||
targetToolParams.default_value = hide ? default_value?.value : '';
|
||||
});
|
||||
};
|
||||
|
||||
export const findToolParamsByComponent = (
|
||||
params: Array<ToolParams>,
|
||||
component: Components,
|
||||
) => params?.find(param => param.name === component.parameter);
|
||||
|
||||
// 初始化query类型的表单参数
|
||||
const mutableInitQueryFormValues = (values: ShortcutEditFormValues): void => {
|
||||
values.tool_type = undefined;
|
||||
values.plugin_id = '';
|
||||
values.work_flow_id = '';
|
||||
values.plugin_api_name = '';
|
||||
values.tool_info = {
|
||||
tool_name: '',
|
||||
tool_params_list: [],
|
||||
};
|
||||
values.components_list = [];
|
||||
values.card_schema = '';
|
||||
};
|
||||
|
||||
// 初始化使用插件组件的时候表单参数
|
||||
const mutableInitNotUseToolFormValues = (
|
||||
values: ShortcutEditFormValues,
|
||||
): void => {
|
||||
values.tool_type = undefined;
|
||||
values.plugin_id = '';
|
||||
values.work_flow_id = '';
|
||||
values.plugin_api_name = '';
|
||||
values.tool_info = {
|
||||
tool_name: '',
|
||||
tool_params_list: [],
|
||||
};
|
||||
values.components_list?.forEach(com => {
|
||||
com.parameter = '';
|
||||
});
|
||||
};
|
||||
|
||||
const mutableSetCardSchemaForForm = (values: ShortcutEditFormValues): void => {
|
||||
const { components_list } = values;
|
||||
const templateDsl = components_list
|
||||
? getDSLFromComponents(components_list)
|
||||
: '';
|
||||
values.card_schema = JSON.stringify(templateDsl);
|
||||
};
|
||||
|
||||
const mutableFormatCommandName = (values: ShortcutEditFormValues): void => {
|
||||
const { shortcut_command } = values;
|
||||
if (shortcut_command) {
|
||||
values.shortcut_command = `/${shortcut_command.trim()}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 筛选toolParams存在,components中不存在的变量
|
||||
* 并且refer_component=false,
|
||||
* 转化为components_list
|
||||
* 用于向前兼容,旧指令中tool的默认参数放在toolParams中
|
||||
*/
|
||||
export const initComponentsListFromToolParams = (
|
||||
components: Components[],
|
||||
toolParams: Array<ToolParams>,
|
||||
): Array<Components> => {
|
||||
const newComponents = components.slice();
|
||||
toolParams.forEach(param => {
|
||||
const { name, default_value, desc, refer_component } = param;
|
||||
if (!components.find(com => com.parameter === name)) {
|
||||
newComponents.push({
|
||||
name,
|
||||
description: desc,
|
||||
parameter: name,
|
||||
input_type: InputType.TextInput,
|
||||
hide: !refer_component,
|
||||
default_value: {
|
||||
type: InputType.TextInput,
|
||||
value: default_value,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return newComponents;
|
||||
};
|
||||
|
||||
/**
|
||||
* 兼容旧指令
|
||||
* 如果InputType为 UploadImage, UploadDoc, UploadTable, UploadAudio,
|
||||
* 判断upload_options是否为空
|
||||
* 为空,加上对应的upload_options
|
||||
*/
|
||||
export const initComponentsUploadOptions = (
|
||||
components: Components[],
|
||||
): Components[] =>
|
||||
components.map(com => {
|
||||
const { input_type, upload_options } = com;
|
||||
if (
|
||||
!upload_options?.length &&
|
||||
input_type &&
|
||||
[
|
||||
InputType.UploadImage,
|
||||
InputType.UploadDoc,
|
||||
InputType.UploadTable,
|
||||
InputType.UploadAudio,
|
||||
].includes(input_type)
|
||||
) {
|
||||
return {
|
||||
...com,
|
||||
upload_options: [input_type],
|
||||
};
|
||||
}
|
||||
return com;
|
||||
});
|
||||
|
||||
export const getInitialValues = (
|
||||
initShortcut?: ShortCutCommand,
|
||||
): ShortcutEditFormValues => {
|
||||
// 初始化
|
||||
if (!initShortcut) {
|
||||
return {
|
||||
send_type: SendType.SendTypeQuery,
|
||||
use_tool: false,
|
||||
};
|
||||
}
|
||||
// 回显
|
||||
const {
|
||||
shortcut_command,
|
||||
tool_type,
|
||||
components_list,
|
||||
tool_info: { tool_params_list = [] } = {},
|
||||
} = initShortcut;
|
||||
const modifyComponentsListByToolParams = initComponentsListFromToolParams(
|
||||
components_list ?? [],
|
||||
tool_params_list,
|
||||
);
|
||||
const modifyComponentsListByUploadOptions = initComponentsUploadOptions(
|
||||
modifyComponentsListByToolParams,
|
||||
);
|
||||
|
||||
return {
|
||||
...initShortcut,
|
||||
shortcut_command: shortcut_command?.replace(/^\//, ''),
|
||||
use_tool: initToolEnabledByToolTYpe(tool_type),
|
||||
components_list: modifyComponentsListByUploadOptions,
|
||||
};
|
||||
};
|
||||
|
||||
export const enableSendTypePanelHideTemplate = (shortcut?: ShortCutCommand) => {
|
||||
if (shortcut?.send_type !== SendType.SendTypePanel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { tool_params_list, tool_name } = shortcut?.tool_info ?? {};
|
||||
|
||||
if (tool_name) {
|
||||
return (
|
||||
!!tool_params_list?.length &&
|
||||
tool_params_list.every(c => !c.refer_component)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
!!shortcut?.components_list?.length &&
|
||||
shortcut.components_list.every(c => c.hide)
|
||||
);
|
||||
};
|
||||
|
||||
export const getFormValueFromShortcut = (shortcut?: ShortCutCommand) => {
|
||||
const { tool_params_list, tool_name } = shortcut?.tool_info ?? {};
|
||||
|
||||
if (tool_name) {
|
||||
if (!tool_params_list?.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return tool_params_list.reduce((prev: Record<string, string>, curr) => {
|
||||
const key = curr.name;
|
||||
const defaultValue = curr?.default_value;
|
||||
|
||||
if (!key || !defaultValue) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
prev[key] = defaultValue;
|
||||
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (!shortcut?.components_list?.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return shortcut.components_list.reduce(
|
||||
(prev: Record<string, string>, curr) => {
|
||||
const key = curr.name;
|
||||
const { value } = curr?.default_value ?? {};
|
||||
if (!key || !value) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
prev[key] = value;
|
||||
|
||||
return prev;
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type Dispatch,
|
||||
type FC,
|
||||
type SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import cls from 'classnames';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { useMultiAgentStore } from '@coze-studio/bot-detail-store/multi-agent';
|
||||
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { Form, UIModal, type UIModalProps } from '@coze-arch/bot-semi';
|
||||
import { PluginStatus } from '@coze-arch/bot-api/plugin_develop';
|
||||
import { ToolType, BotMode } from '@coze-arch/bot-api/playground_api';
|
||||
import { PluginDevelopApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type ShortcutEditFormValues, type SkillsModalProps } from '../types';
|
||||
import {
|
||||
validateCmdString,
|
||||
validateCommandNameRepeat,
|
||||
} from '../../utils/tool-params';
|
||||
import { ShortcutTemplate } from '../../shortcut-template';
|
||||
import { SwitchAgent } from './switch-agent';
|
||||
import { getInitialValues, getSubmitValue } from './method';
|
||||
import { FieldLabel } from './components/field-label';
|
||||
import { FormInputWithMaxCount } from './components';
|
||||
import { ButtonName } from './button-name';
|
||||
import {
|
||||
ActionSwitchArea,
|
||||
type IActionSwitchAreaRef,
|
||||
} from './action-switch-area';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
export interface ShortcutEditModalProps
|
||||
extends Omit<UIModalProps, 'onOk' | 'onCancel'> {
|
||||
errorMessage: string;
|
||||
setErrorMessage: Dispatch<SetStateAction<string>>;
|
||||
shortcut?: ShortCutCommand;
|
||||
skillModal: FC<SkillsModalProps>;
|
||||
onAdd?: (shortcuts: ShortCutCommand, onFail: () => void) => void;
|
||||
onEdit?: (shortcuts: ShortCutCommand, onFail: () => void) => void;
|
||||
botMode: BotMode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const ShortcutEditModal: FC<ShortcutEditModalProps> = props => {
|
||||
const {
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
shortcut,
|
||||
onAdd,
|
||||
onEdit,
|
||||
skillModal: SkillModal,
|
||||
onClose,
|
||||
botMode,
|
||||
} = props;
|
||||
const { botId, spaceId } = useBotInfoStore(
|
||||
useShallow(state => ({
|
||||
botId: state.botId,
|
||||
spaceId: state.space_id,
|
||||
})),
|
||||
);
|
||||
const { agents } = useMultiAgentStore(
|
||||
useShallow(state => ({
|
||||
agents: state.agents,
|
||||
})),
|
||||
);
|
||||
const { existedShortcuts } = useBotSkillStore(
|
||||
useShallow(state => ({
|
||||
existedShortcuts: state.shortcut.shortcut_list,
|
||||
})),
|
||||
);
|
||||
|
||||
const formRef = useRef<Form>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const actionSwitchAreaRef = useRef<IActionSwitchAreaRef>(null);
|
||||
const { TextArea } = Form;
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
|
||||
const [editedShortcut, setEditedShortcut] = useState<ShortcutEditFormValues>(
|
||||
getInitialValues(shortcut),
|
||||
);
|
||||
|
||||
// 使用技能 & 未选择工具 => 禁止提交
|
||||
const disableSubmit =
|
||||
editedShortcut?.use_tool && !editedShortcut?.tool_info?.tool_name;
|
||||
|
||||
const showPanel = !!editedShortcut?.components_list?.filter(
|
||||
comp => !comp.hide,
|
||||
).length;
|
||||
|
||||
const mode = shortcut ? 'edit' : 'create';
|
||||
|
||||
const onConfirm = async () => {
|
||||
setConfirmLoading(true);
|
||||
if (!(await checkFormValid())) {
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
const values = formRef.current?.formApi.getValues();
|
||||
const formattedValues = getSubmitValue(values);
|
||||
console.log('onConfirm', formattedValues);
|
||||
|
||||
if (mode === 'create') {
|
||||
// TODO: hzf add的类型应该没有command_id
|
||||
onAdd?.(formattedValues, () => {
|
||||
setConfirmLoading(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'edit') {
|
||||
onEdit?.(formattedValues, () => {
|
||||
setConfirmLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkFormValid = async () => {
|
||||
try {
|
||||
await formRef.current?.formApi.validate();
|
||||
|
||||
return actionSwitchAreaRef.current?.validate();
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch -- 正常表单校验不需要处理e
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onFormValueChange = (values?: ShortcutEditFormValues) => {
|
||||
setErrorMessage('');
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
setEditedShortcut({ ...values });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.formApi.setValue('object_id', botId);
|
||||
}, []);
|
||||
|
||||
const { data: pluginData } = useRequest(
|
||||
async () => {
|
||||
// 方便类型推断
|
||||
if (shortcut?.plugin_id && spaceId) {
|
||||
const res = await PluginDevelopApi.GetPlaygroundPluginList({
|
||||
page: 1,
|
||||
size: 1,
|
||||
plugin_ids: [shortcut.plugin_id],
|
||||
space_id: spaceId,
|
||||
is_get_offline: true,
|
||||
});
|
||||
return res.data?.plugin_list?.[0];
|
||||
}
|
||||
},
|
||||
{
|
||||
ready: !!(shortcut?.plugin_id && spaceId),
|
||||
},
|
||||
);
|
||||
|
||||
const isBanned =
|
||||
shortcut?.tool_type === ToolType.ToolTypePlugin &&
|
||||
shortcut?.plugin_id === pluginData?.id &&
|
||||
pluginData?.status === PluginStatus.BANNED;
|
||||
|
||||
return (
|
||||
<>
|
||||
<UIModal
|
||||
{...props}
|
||||
visible
|
||||
footer={null}
|
||||
onCancel={onClose}
|
||||
className={style['shortcut-edit-modal']}
|
||||
bodyStyle={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
minHeight: 0,
|
||||
}}
|
||||
width={showPanel ? 1120 : 670}
|
||||
title={
|
||||
mode === 'create'
|
||||
? I18n.t('shortcut_modal_title')
|
||||
: I18n.t('shortcut_modal_title_edit_shortcut')
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
style['edit-modal-wrapper'],
|
||||
showPanel && style.wrapperBorder,
|
||||
)}
|
||||
ref={modalRef}
|
||||
contentEditable={false}
|
||||
>
|
||||
<Form<ShortcutEditFormValues>
|
||||
ref={formRef}
|
||||
trigger="blur"
|
||||
initValues={editedShortcut}
|
||||
autoComplete={'off'}
|
||||
autoScrollToError
|
||||
className={cls(style['edit-form-wrapper'], {
|
||||
'pr-6': showPanel,
|
||||
})}
|
||||
onValueChange={values => onFormValueChange(values)}
|
||||
>
|
||||
<div className={style['form-item']}>
|
||||
<FieldLabel required>
|
||||
{I18n.t('shortcut_modal_button_name')}
|
||||
</FieldLabel>
|
||||
<ButtonName editedShortcut={editedShortcut} />
|
||||
</div>
|
||||
<div className={style['form-item']}>
|
||||
<FieldLabel
|
||||
tip={I18n.t('shortcut_modal_shortcut_name_input_placeholder')}
|
||||
required
|
||||
>
|
||||
{I18n.t('shortcut_modal_shortcut_name')}
|
||||
</FieldLabel>
|
||||
<FormInputWithMaxCount
|
||||
required
|
||||
noLabel
|
||||
prefix="/"
|
||||
maxCount={20}
|
||||
maxLength={20}
|
||||
field="shortcut_command"
|
||||
placeholder={I18n.t(
|
||||
'shortcut_modal_shortcut_name_input_placeholder',
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('shortcut_modal_shortcut_name_is_required'),
|
||||
},
|
||||
{
|
||||
validator: (rule, value) => validateCmdString(value),
|
||||
message: I18n.t(
|
||||
'shortcut_modal_use_at_least_one_letter_error',
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: (rule, value) =>
|
||||
validateCommandNameRepeat(
|
||||
{
|
||||
...editedShortcut,
|
||||
shortcut_command: `/${value}`,
|
||||
},
|
||||
existedShortcuts ?? [],
|
||||
),
|
||||
message: I18n.t(
|
||||
'shortcut_modal_shortcut_name_conflict_error',
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className={style['form-item']}>
|
||||
<FieldLabel>
|
||||
{I18n.t('shortcut_modal_shortcut_description')}
|
||||
</FieldLabel>
|
||||
<TextArea
|
||||
maxCount={100}
|
||||
maxLength={100}
|
||||
rows={3}
|
||||
placeholder={I18n.t(
|
||||
'shortcut_modal_shortcut_description_input_placeholder',
|
||||
)}
|
||||
field="description"
|
||||
noLabel
|
||||
/>
|
||||
</div>
|
||||
<ActionSwitchArea
|
||||
ref={actionSwitchAreaRef}
|
||||
editedShortcut={editedShortcut}
|
||||
skillModal={SkillModal}
|
||||
formRef={formRef}
|
||||
modalRef={modalRef}
|
||||
isBanned={isBanned}
|
||||
/>
|
||||
{botMode === BotMode.MultiMode && (
|
||||
<SwitchAgent
|
||||
editedShortcut={editedShortcut}
|
||||
showPanel={showPanel}
|
||||
agents={agents}
|
||||
formRef={formRef}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
{showPanel && editedShortcut ? (
|
||||
<div className={style['preview-component']}>
|
||||
<div className={style['shortcut-panel']}>
|
||||
<ShortcutTemplate
|
||||
visible={true}
|
||||
shortcut={editedShortcut}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Form.ErrorMessage
|
||||
className="flex-1 text-left mt-0"
|
||||
error={errorMessage || ''}
|
||||
/>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
color="highlight"
|
||||
className="!coz-mg-hglt !coz-fg-hglt"
|
||||
>
|
||||
{I18n.t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
loading={confirmLoading}
|
||||
disabled={disableSubmit}
|
||||
>
|
||||
{I18n.t('Confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</UIModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type RefObject, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import { type Agent } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { type Form, Popover, Typography, Input } from '@coze-arch/bot-semi';
|
||||
import { PlaygroundApi } from '@coze-arch/bot-api';
|
||||
import { IconChevronDown } from '@douyinfe/semi-icons';
|
||||
|
||||
import styles from '../index.module.less';
|
||||
import FieldLabel from '../components/field-label';
|
||||
import type { ShortcutEditFormValues } from '../../types';
|
||||
import { type ItemType } from '../../../utils/data-helper';
|
||||
import { LoadMoreList } from '../../../components/load-more-list';
|
||||
import SelectCheck from '../../../assets/select-check.png';
|
||||
import AgentIcon from '../../../assets/agent-icon.png';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
interface ResultData {
|
||||
list: {
|
||||
agentName: string;
|
||||
agentId: string;
|
||||
}[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface SwitchAgentProps {
|
||||
formRef: RefObject<Form>;
|
||||
showPanel: boolean;
|
||||
agents: Agent[];
|
||||
editedShortcut: ShortcutEditFormValues;
|
||||
}
|
||||
|
||||
export const SwitchAgent = (props: SwitchAgentProps) => {
|
||||
const { formRef, showPanel, editedShortcut, agents } = props;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const popRef = useRef<Popover>(null);
|
||||
const [isShowLoadMoreList, setIsShowLoadMoreList] = useState(false);
|
||||
const { defaultAgentId, defaultName } = useMemo(() => {
|
||||
if (!editedShortcut.agent_id) {
|
||||
return {
|
||||
defaultAgentId: '',
|
||||
// @ts-expect-error --后面替换
|
||||
defaultName: I18n.t('Do not specify'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
defaultAgentId: editedShortcut.agent_id,
|
||||
defaultName:
|
||||
agents.find(agent => agent.id === editedShortcut.agent_id)?.name ?? '',
|
||||
};
|
||||
}, [editedShortcut.agent_id]);
|
||||
const [inputValue, setInputValue] = useState(defaultName);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
ref={popRef}
|
||||
content={
|
||||
<AgentLoadMoreList
|
||||
defaultSelectedId={defaultAgentId}
|
||||
showPanel={showPanel}
|
||||
onSelect={item => {
|
||||
formRef.current?.formApi.setValue('agent_id', item.agentId);
|
||||
setInputValue(item.agentName);
|
||||
setIsShowLoadMoreList(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
keepDOM
|
||||
onVisibleChange={setIsShowLoadMoreList}
|
||||
autoAdjustOverflow={false}
|
||||
position={'bottomLeft'}
|
||||
trigger="custom"
|
||||
visible={isShowLoadMoreList}
|
||||
onClickOutSide={() => setIsShowLoadMoreList(false)}
|
||||
onEscKeyDown={() => setIsShowLoadMoreList(false)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
'w-full pb-[32px]',
|
||||
styles['switch-agent-input-wrapper'],
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsShowLoadMoreList(!isShowLoadMoreList);
|
||||
}}
|
||||
>
|
||||
<FieldLabel>
|
||||
{I18n.t('multiagent_shortcut_modal_specify_node')}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
suffix={<IconChevronDown />}
|
||||
className="w-full hover:!coz-mg-secondary-hovered active:!coz-mg-secondary-pressed"
|
||||
readonly={true}
|
||||
value={inputValue}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface AgentLoadMoreListProps {
|
||||
onSelect: (item: ItemType<ResultData['list']>) => void;
|
||||
defaultSelectedId?: string;
|
||||
showPanel: boolean;
|
||||
}
|
||||
|
||||
const AgentLoadMoreList = (props: AgentLoadMoreListProps) => {
|
||||
const { onSelect, showPanel, defaultSelectedId } = props;
|
||||
const [activeId, setActiveId] = useState('');
|
||||
const [selectedId, setSelectedId] = useState('');
|
||||
const botId = useBotInfoStore(state => state.botId);
|
||||
const getSpaceId = () => useSpaceStore.getState().getSpaceId();
|
||||
|
||||
return (
|
||||
<LoadMoreList<ItemType<ResultData['list']>>
|
||||
defaultId={defaultSelectedId}
|
||||
className={cls(
|
||||
'max-h-[122px] p-1 overflow-y-auto cursor-pointer overflow-x-hidden',
|
||||
styles['load-more-list'],
|
||||
)}
|
||||
style={{
|
||||
width: showPanel ? '687px' : '590px',
|
||||
}}
|
||||
getId={item => item.agentId}
|
||||
defaultList={[
|
||||
{
|
||||
agentId: '',
|
||||
agentName: I18n.t(
|
||||
'multiagent_shortcut_modal_specify_node_option_do_not_specify',
|
||||
),
|
||||
},
|
||||
]}
|
||||
getMoreListService={currentData => {
|
||||
const page = currentData
|
||||
? Math.ceil(currentData.list.length / PAGE_SIZE) + 1
|
||||
: 1;
|
||||
return getAgentList({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
botId,
|
||||
spaceId: getSpaceId(),
|
||||
});
|
||||
}}
|
||||
onActiveId={id => setActiveId(id)}
|
||||
onSelect={item => {
|
||||
setSelectedId(item.agentId);
|
||||
}}
|
||||
itemRender={item => (
|
||||
<AgentItem
|
||||
onClick={onSelect}
|
||||
activeId={activeId}
|
||||
selectedId={selectedId}
|
||||
data={item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface AgentItemProps {
|
||||
activeId: string;
|
||||
selectedId: string;
|
||||
data: ItemType<ResultData['list']>;
|
||||
onClick?: (item: ItemType<ResultData['list']>) => void;
|
||||
}
|
||||
|
||||
const AgentItem = (renderProps: AgentItemProps) => {
|
||||
const { onClick, activeId, data, selectedId } = renderProps;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'flex justify-start p-2 items-center h-8 cursor-pointer w-full',
|
||||
{
|
||||
'rounded border coz-stroke-plus coz-mg-secondary-hovered':
|
||||
activeId === data.agentId,
|
||||
},
|
||||
)}
|
||||
onClick={() => onClick?.(data)}
|
||||
>
|
||||
<img
|
||||
alt="checked"
|
||||
src={SelectCheck}
|
||||
className="w-4 h-4 ml-2"
|
||||
style={{
|
||||
visibility: [activeId, selectedId].includes(data.agentId)
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
}}
|
||||
/>
|
||||
<img alt="icon" src={AgentIcon} className="mr-2 w-4 h-4 ml-2" />
|
||||
<Text ellipsis className="w-full coz-fg-primary text-sm">
|
||||
{data.agentName}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getAgentList = async (props: {
|
||||
botId: string;
|
||||
spaceId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}): Promise<ResultData> => {
|
||||
try {
|
||||
const { botId, spaceId, page, pageSize } = props;
|
||||
const res = await PlaygroundApi.GetShortcutAvailNodes({
|
||||
bot_id: botId,
|
||||
space_id: spaceId,
|
||||
page_num: page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
const { nodes, has_more } = res.data;
|
||||
return {
|
||||
list: nodes.map(item => ({
|
||||
agentName: item.agent_name,
|
||||
agentId: item.agent_id,
|
||||
})),
|
||||
hasMore: has_more,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('getAgentListError', e);
|
||||
return {
|
||||
list: [],
|
||||
hasMore: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
useEffect,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Popover, Typography } from '@coze-arch/bot-semi';
|
||||
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { InputTypeTag } from './var-list';
|
||||
import { componentTypeOptionMap } from './util';
|
||||
|
||||
export interface ComponentsSelectPopoverProps {
|
||||
autoFocusFirst?: boolean;
|
||||
visible: boolean;
|
||||
components?: ValidComponents[];
|
||||
onClose: () => void;
|
||||
onChange: (component: ValidComponents) => void;
|
||||
}
|
||||
|
||||
export interface ComponentsSelectPopoverActions {
|
||||
setHover: Dispatch<SetStateAction<number>>;
|
||||
select: () => void;
|
||||
}
|
||||
|
||||
export type ValidComponents = shortcut_command.Components &
|
||||
Required<Pick<shortcut_command.Components, 'input_type' | 'name'>>;
|
||||
|
||||
export const ComponentsSelectPopover = forwardRef<
|
||||
ComponentsSelectPopoverActions,
|
||||
PropsWithChildren<ComponentsSelectPopoverProps>
|
||||
>(
|
||||
(
|
||||
{ components = [], visible, onChange, children, onClose, autoFocusFirst },
|
||||
ref,
|
||||
) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
setHover: param => {
|
||||
const newIndex = typeof param === 'number' ? param : param(hoverIndex);
|
||||
const targetDom = optionsDomRef.current[newIndex];
|
||||
setHoverIndex(newIndex);
|
||||
if (targetDom) {
|
||||
targetDom.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
},
|
||||
select: () => {
|
||||
const hoverComponent = components[hoverIndex];
|
||||
hoverComponent && onChange(hoverComponent);
|
||||
onClose();
|
||||
},
|
||||
}));
|
||||
|
||||
const optionsDomRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const [hoverIndex, setHoverIndex] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setHoverIndex(autoFocusFirst ? 0 : -1);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
trigger="custom"
|
||||
onEscKeyDown={onClose}
|
||||
onClickOutSide={onClose}
|
||||
onVisibleChange={newVisible => {
|
||||
if (!newVisible) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
position="bottom"
|
||||
className="!rounded-[8px]"
|
||||
content={
|
||||
<div
|
||||
onMouseLeave={() => {
|
||||
setHoverIndex(-1);
|
||||
}}
|
||||
className="p-1 max-h-44 overflow-y-auto box-border"
|
||||
>
|
||||
{components
|
||||
.filter(item => item.input_type !== undefined && item.name)
|
||||
.map((item, index) => {
|
||||
const type = componentTypeOptionMap[item.input_type]?.label;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.name}
|
||||
ref={el => {
|
||||
optionsDomRef.current[index] = el;
|
||||
}}
|
||||
onMouseEnter={() => setHoverIndex(index)}
|
||||
className={classNames(
|
||||
'flex items-center px-2 h-8 gap-2 cursor-pointer rounded-[4px]',
|
||||
{
|
||||
'coz-mg-secondary-hovered': index === hoverIndex,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(item);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: true,
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
{type ? <InputTypeTag>{type}</InputTypeTag> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!components.length && (
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: true,
|
||||
}}
|
||||
style={{ color: 'rgba(29, 28, 35, 0.35)' }}
|
||||
className="flex-1 p-2.5 coz-fg-secondary text-xs"
|
||||
>
|
||||
{I18n.t('shortcut_modal_query_message_insert_component_button')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CSSProperties,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import {
|
||||
ExpressionEditorEvent,
|
||||
ExpressionEditorRender,
|
||||
type ExpressionEditorTreeNode,
|
||||
} from '@coze-workflow/sdk';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
|
||||
import { VarExpressionEditorSuggestion } from './suggestion';
|
||||
import { VarExpressionEditorModel } from './model';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ExpressionEditorContainerProps {
|
||||
value: string;
|
||||
getPopupContainer?: PopoverProps['getPopupContainer'];
|
||||
variableTree: ExpressionEditorTreeNode[];
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ExpressionEditorContainerRef {
|
||||
model: VarExpressionEditorModel;
|
||||
}
|
||||
|
||||
const ExpressionEditorContainer = forwardRef<
|
||||
ExpressionEditorContainerRef,
|
||||
ExpressionEditorContainerProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
variableTree,
|
||||
placeholder,
|
||||
onChange,
|
||||
readonly = false,
|
||||
style,
|
||||
className,
|
||||
getPopupContainer,
|
||||
} = props;
|
||||
|
||||
const [focus, _setFocus] = useState<boolean>(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const formValue: string = props.value || '';
|
||||
const [model] = useState<VarExpressionEditorModel>(
|
||||
() => new VarExpressionEditorModel(formValue),
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({ model }));
|
||||
|
||||
useEffect(() => model.setVariableTree(variableTree), [variableTree]);
|
||||
useEffect(() => model.setFocus(focus), [focus]);
|
||||
|
||||
// 同步表单值变化
|
||||
useEffect(() => {
|
||||
if (model.value === formValue) {
|
||||
// 无需同步
|
||||
return;
|
||||
}
|
||||
model.setValue(formValue);
|
||||
}, [formValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const disposer = model.on<ExpressionEditorEvent.Change>(
|
||||
ExpressionEditorEvent.Change,
|
||||
(params: { value: string }) => onChange?.(params.value),
|
||||
);
|
||||
return () => {
|
||||
disposer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!model?.variableTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(className, styles.container)}
|
||||
style={style}
|
||||
ref={containerRef}
|
||||
>
|
||||
<ExpressionEditorRender
|
||||
model={model}
|
||||
className={styles.editorRender}
|
||||
readonly={readonly}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{readonly ? null : (
|
||||
<VarExpressionEditorSuggestion
|
||||
model={model}
|
||||
containerRef={containerRef}
|
||||
getPopupContainer={getPopupContainer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default ExpressionEditorContainer;
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type FC,
|
||||
type RefObject,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { type ExpressionEditorTreeNode } from '@coze-workflow/sdk';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type CommonFieldProps } from '@coze-arch/coze-design';
|
||||
import { withField, UIIconButton, useFormState } from '@coze-arch/bot-semi';
|
||||
import { IconCopyLink } from '@coze-arch/bot-icons';
|
||||
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { queryTip } from '../components/tip';
|
||||
import FieldLabel from '../components/field-label';
|
||||
import btnStyles from '../components/action-button/index.module.less';
|
||||
import { type VarTreeNode } from './type';
|
||||
import type { ExpressionEditorContainerRef } from './container';
|
||||
import {
|
||||
ComponentsSelectPopover,
|
||||
type ValidComponents,
|
||||
} from './components-select';
|
||||
import VarQueryTextarea, { type UsageWithVarTextAreaProps } from '.';
|
||||
|
||||
const VarQueryTextareaWithField: FC<
|
||||
CommonFieldProps & UsageWithVarTextAreaProps
|
||||
> = withField(VarQueryTextarea);
|
||||
|
||||
type VProps = CommonFieldProps & Pick<UsageWithVarTextAreaProps, 'value'>;
|
||||
|
||||
interface VarQueryTextareaWrapper extends VProps {
|
||||
components?: shortcut_command.Components[];
|
||||
modalRef?: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const maxCount = 3000;
|
||||
|
||||
const VarQueryTextareaWrapperWithField: FC<VarQueryTextareaWrapper> = props => {
|
||||
const { components, modalRef, ...innerProps } = props;
|
||||
const { value, field } = props;
|
||||
const [showLinBtnPopup, setShowLinkBtnPopup] = useState(false);
|
||||
const editorRef = useRef<ExpressionEditorContainerRef>(null);
|
||||
const { errors } = useFormState();
|
||||
const isErrorStatus = !!(field && errors && field in errors);
|
||||
|
||||
const validComponents = useMemo(() => {
|
||||
if (!components?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return components.filter(
|
||||
(item): item is ValidComponents =>
|
||||
item.input_type !== undefined && !!item.name,
|
||||
);
|
||||
}, [components]);
|
||||
|
||||
const variableList = validComponents.map(
|
||||
({ name, input_type }) =>
|
||||
({
|
||||
label: name,
|
||||
value: name,
|
||||
key: name,
|
||||
varInputType: input_type,
|
||||
} satisfies VarTreeNode),
|
||||
);
|
||||
const hasComponents = !!validComponents?.length;
|
||||
const placeholder = hasComponents
|
||||
? I18n.t('shortcut_modal_query_message_placeholder')
|
||||
: I18n.t('shortcut_modal_query_content_input_placeholder');
|
||||
|
||||
const onComponentsSelectChange = (component: ValidComponents) => {
|
||||
const newValue = `${value ?? ''}{{${component.name}}}`;
|
||||
editorRef.current?.model.insertText(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel
|
||||
tooltip={{ className: '!max-w-[370px]' }}
|
||||
tip={queryTip()}
|
||||
required
|
||||
>
|
||||
{I18n.t('shortcut_modal_query_content')}
|
||||
</FieldLabel>
|
||||
{hasComponents ? (
|
||||
<ComponentsSelectPopover
|
||||
visible={showLinBtnPopup}
|
||||
components={validComponents}
|
||||
onClose={() => {
|
||||
setShowLinkBtnPopup(false);
|
||||
}}
|
||||
onChange={onComponentsSelectChange}
|
||||
>
|
||||
<UIIconButton
|
||||
icon={<IconCopyLink />}
|
||||
wrapperClass={btnStyles.btn}
|
||||
onClick={() => {
|
||||
setShowLinkBtnPopup(!showLinBtnPopup);
|
||||
}}
|
||||
>
|
||||
{I18n.t('shortcut_modal_query_insert_component_tip')}
|
||||
</UIIconButton>
|
||||
</ComponentsSelectPopover>
|
||||
) : null}
|
||||
</div>
|
||||
<VarQueryTextareaWithField
|
||||
{...innerProps}
|
||||
variableProps={{
|
||||
variableList: variableList as ExpressionEditorTreeNode[],
|
||||
getPopupContainer: () => modalRef?.current ?? document.body,
|
||||
editorRef,
|
||||
isErrorStatus,
|
||||
}}
|
||||
trigger={['blur', 'change']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('shortcut_modal_query_content_is_required'),
|
||||
},
|
||||
{
|
||||
max: maxCount,
|
||||
message: I18n.t(
|
||||
'shortcut_modal_query_message_max_length_reached_error',
|
||||
),
|
||||
},
|
||||
]}
|
||||
placeholder={placeholder}
|
||||
maxCount={maxCount}
|
||||
rows={3}
|
||||
fieldClassName="!pt-0 !pb-[16px]"
|
||||
noLabel
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VarQueryTextareaWrapperWithField;
|
||||
@@ -0,0 +1,78 @@
|
||||
@height: calc(var(--studio-var-textarea-line-height) * 1px);
|
||||
|
||||
.editor-render {
|
||||
cursor: text;
|
||||
resize: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
vertical-align: bottom;
|
||||
|
||||
background-color: transparent;
|
||||
border: 0 solid transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
padding: 5px 12px;
|
||||
|
||||
line-height: 22px;
|
||||
color: var(--semi-color-text-0);
|
||||
|
||||
background-color: var(--semi-color-white);
|
||||
border: 1px var(--semi-color-border) solid;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--semi-color-fill-0);
|
||||
}
|
||||
|
||||
&.focus {
|
||||
background-color: var(--semi-color-fill-0);
|
||||
border: 1px var(--semi-color-primary) solid;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px var(--semi-color-danger) solid;
|
||||
|
||||
&:hover,
|
||||
&.focus {
|
||||
background-color: var(--semi-color-danger-light-default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow-y: auto;
|
||||
height: @height;
|
||||
|
||||
:global {
|
||||
div > div > div[data-slate-editor='true'] {
|
||||
min-height: @height !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 24px;
|
||||
padding: 3px 0 5px;
|
||||
|
||||
font-size: 12px;
|
||||
color: var(--semi-color-text-2);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type FC,
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import cs from 'classnames';
|
||||
import { type ExpressionEditorTreeNode } from '@coze-workflow/sdk';
|
||||
import { type TextAreaProps } from '@coze-arch/coze-design';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
|
||||
import { getCssVarStyle } from './util';
|
||||
import ExpressionEditorContainer, {
|
||||
type ExpressionEditorContainerRef,
|
||||
} from './container';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface UsageWithVarTextAreaProps
|
||||
extends Pick<
|
||||
TextAreaProps,
|
||||
'maxCount' | 'rows' | 'value' | 'style' | 'placeholder'
|
||||
> {
|
||||
onChange?: (value: string) => void;
|
||||
variableProps?: {
|
||||
variableList: ExpressionEditorTreeNode[];
|
||||
getPopupContainer?: PopoverProps['getPopupContainer'];
|
||||
editorRef?: RefObject<ExpressionEditorContainerRef>;
|
||||
isErrorStatus?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const debounceMs = 100;
|
||||
|
||||
const VarQueryTextarea: FC<UsageWithVarTextAreaProps> = props => {
|
||||
const { maxCount, rows, style, value, onChange, placeholder, variableProps } =
|
||||
props;
|
||||
const {
|
||||
variableList = [],
|
||||
getPopupContainer,
|
||||
editorRef: propEditorRef,
|
||||
isErrorStatus = false,
|
||||
} = variableProps ?? {};
|
||||
const editorRef = useRef<ExpressionEditorContainerRef>(null);
|
||||
const [focus, _setFocus] = useState<boolean>(false);
|
||||
const showMaxCount = typeof maxCount === 'number';
|
||||
const scroll = typeof rows === 'number';
|
||||
const cssVarsStyle = getCssVarStyle({ rows, style });
|
||||
const count = value ? value.length : 0;
|
||||
|
||||
useEffect(() => editorRef.current?.model.setFocus(focus), [focus]);
|
||||
|
||||
// 设置防抖防止 onFocus / onBlur 在点击时出现抖动
|
||||
const setFocus = useCallback(
|
||||
debounce((newFocusValue: boolean) => {
|
||||
_setFocus(newFocusValue);
|
||||
}, debounceMs),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(
|
||||
styles.textarea,
|
||||
focus && styles.focus,
|
||||
isErrorStatus && styles.error,
|
||||
)}
|
||||
style={cssVarsStyle}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
>
|
||||
<div className={scroll ? styles.scroller : undefined}>
|
||||
<ExpressionEditorContainer
|
||||
ref={propEditorRef ?? editorRef}
|
||||
value={value ?? ''}
|
||||
onChange={onChange}
|
||||
variableTree={variableList}
|
||||
placeholder={placeholder}
|
||||
getPopupContainer={getPopupContainer}
|
||||
/>
|
||||
</div>
|
||||
{showMaxCount ? (
|
||||
<div className={styles.footer}>
|
||||
{count}/{maxCount}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VarQueryTextarea;
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionEditorModel } from '@coze-workflow/sdk';
|
||||
|
||||
export class VarExpressionEditorModel extends ExpressionEditorModel {
|
||||
public insertText = (text: string) => {
|
||||
this.editor.insertText(text);
|
||||
this.setValue(text);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable max-nesting-depth */
|
||||
.expression-editor-suggestion-pin {
|
||||
position: absolute;
|
||||
transform: translateY(-0.5rem);
|
||||
width: 0;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.expression-editor-suggestion {
|
||||
z-index: 1000;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
width: 272px;
|
||||
max-height: 236px;
|
||||
|
||||
background: var(--light-usage-bg-color-bg-3, #fff);
|
||||
border: 0.5px solid rgba(153, 182, 255, 12%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 25%);
|
||||
}
|
||||
|
||||
.expression-editor-suggestion-empty {
|
||||
z-index: 1000;
|
||||
|
||||
background: #fff;
|
||||
border: 0.5px solid rgba(153, 182, 255, 12%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 25%);
|
||||
|
||||
p {
|
||||
margin: 4px 6px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--light-usage-text-color-text-2, rgba(29, 28, 35, 60%));
|
||||
}
|
||||
}
|
||||
|
||||
.expression-editor-suggestion-tree {
|
||||
:global {
|
||||
.semi-tree-search-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.semi-tree-option-list {
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
padding: 6px 6px 6px 0;
|
||||
|
||||
li {
|
||||
height: 32px;
|
||||
margin-top: 4px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option {
|
||||
pointer-events: none;
|
||||
margin-left: 0;
|
||||
background-color: transparent;
|
||||
|
||||
.semi-tree-option-indent,
|
||||
.semi-tree-option-empty-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-label {
|
||||
pointer-events: auto;
|
||||
height: 32px;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--coz-mg-secondary-hovered);
|
||||
}
|
||||
|
||||
.semi-tree-option-label-text {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
|
||||
& span {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.semi-tree-option-highlight {
|
||||
color: var(--light-usage-warning-color-warning, #ff9600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-selected {
|
||||
font-weight: 600;
|
||||
color: var(--light-usage-primary-color-primary, #4d53e8);
|
||||
}
|
||||
|
||||
.semi-tree-option-disabled {
|
||||
.semi-tree-option-label {
|
||||
cursor: not-allowed;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.semi-icon + .semi-tree-option-label {
|
||||
color: var(--light-usage-text-color-text-0, #1d1c23);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-empty-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.semi-tree-option-expand-icon {
|
||||
pointer-events: auto;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 0;
|
||||
padding: 4px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--light-usage-fill-color-fill-1, rgb(46 46 56 / 8%));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--light-usage-fill-color-fill-2, rgb(46 46 56 / 12%));
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-label {
|
||||
color: var(--coz-fg-hglt-yellow);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type FC,
|
||||
type RefObject,
|
||||
type ReactNode,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useListeners,
|
||||
useSelectNode,
|
||||
useKeyboardSelect,
|
||||
useRenderEffect,
|
||||
useSuggestionReducer,
|
||||
type SelectorBoxConfigEntity,
|
||||
type PlaygroundConfigEntity,
|
||||
type ExpressionEditorModel,
|
||||
type ExpressionEditorTreeNode,
|
||||
} from '@coze-workflow/sdk';
|
||||
import { type TreeProps } from '@coze-arch/bot-semi/Tree';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
import { Popover, Tree } from '@coze-arch/bot-semi';
|
||||
|
||||
import { VarListItem, InputTypeTag } from '../var-list';
|
||||
import { componentTypeOptionMap } from '../util';
|
||||
import { type VarTreeNode } from '../type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ExpressionEditorSuggestionProps {
|
||||
className?: string;
|
||||
model: ExpressionEditorModel;
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
getPopupContainer?: PopoverProps['getPopupContainer'];
|
||||
playgroundConfig?: PlaygroundConfigEntity;
|
||||
selectorBoxConfig?: SelectorBoxConfigEntity;
|
||||
treeProps?: Partial<TreeProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动提示
|
||||
*/
|
||||
export const VarExpressionEditorSuggestion: FC<
|
||||
ExpressionEditorSuggestionProps
|
||||
> = props => {
|
||||
const {
|
||||
model,
|
||||
containerRef,
|
||||
className,
|
||||
playgroundConfig,
|
||||
selectorBoxConfig,
|
||||
getPopupContainer = () => containerRef.current ?? document.body,
|
||||
treeProps = {},
|
||||
} = props;
|
||||
const suggestionRef = useRef<HTMLDivElement>(null);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const treeRef = useRef<Tree>(null);
|
||||
|
||||
const suggestionReducer = useSuggestionReducer({
|
||||
model,
|
||||
entities: {
|
||||
playgroundConfig,
|
||||
selectorBoxConfig,
|
||||
},
|
||||
ref: {
|
||||
container: containerRef,
|
||||
suggestion: suggestionRef,
|
||||
tree: treeRef,
|
||||
},
|
||||
});
|
||||
const [state] = suggestionReducer;
|
||||
const selectNode = useSelectNode(suggestionReducer);
|
||||
useRenderEffect(suggestionReducer);
|
||||
useListeners(suggestionReducer);
|
||||
useKeyboardSelect(suggestionReducer, selectNode);
|
||||
|
||||
const renderLabel = (label?: ReactNode, data?: VarTreeNode) => {
|
||||
if (typeof label !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const idx = label.indexOf(searchValue);
|
||||
|
||||
if (idx === -1) {
|
||||
return label;
|
||||
}
|
||||
|
||||
let tag: string | null = null;
|
||||
|
||||
if (typeof data?.varInputType === 'number') {
|
||||
const text = componentTypeOptionMap[data.varInputType]?.label;
|
||||
if (text) {
|
||||
tag = text;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VarListItem>
|
||||
<div>
|
||||
{label.substring(0, idx)}
|
||||
<span className={styles.highlightLabel}>{searchValue}</span>
|
||||
{label.substring(idx + searchValue.length)}
|
||||
</div>
|
||||
{tag ? <InputTypeTag>{tag}</InputTypeTag> : null}
|
||||
</VarListItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
visible={state.visible}
|
||||
keepDOM={true}
|
||||
getPopupContainer={getPopupContainer}
|
||||
content={
|
||||
<>
|
||||
<div
|
||||
className={styles['expression-editor-suggestion-empty']}
|
||||
style={{
|
||||
display:
|
||||
!state.visible || !state.emptyContent ? 'none' : 'inherit',
|
||||
}}
|
||||
>
|
||||
<p>{state.emptyContent}</p>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles['expression-editor-suggestion'],
|
||||
)}
|
||||
ref={suggestionRef}
|
||||
style={{
|
||||
display:
|
||||
!state.visible || state.emptyContent || state.hiddenDOM
|
||||
? 'none'
|
||||
: 'inherit',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Tree
|
||||
{...treeProps}
|
||||
key={state.key}
|
||||
className={classNames(
|
||||
styles['expression-editor-suggestion-tree'],
|
||||
treeProps.className,
|
||||
)}
|
||||
showFilteredOnly
|
||||
filterTreeNode
|
||||
onChangeWithObject
|
||||
ref={treeRef}
|
||||
treeData={state.variableTree}
|
||||
searchRender={false}
|
||||
value={state.selected}
|
||||
emptyContent={<></>}
|
||||
onSelect={(key, selected, node) => {
|
||||
selectNode(node as ExpressionEditorTreeNode);
|
||||
}}
|
||||
renderLabel={renderLabel}
|
||||
onSearch={inputValue => {
|
||||
setSearchValue(inputValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={styles['expression-editor-suggestion-pin']}
|
||||
style={{
|
||||
top: state.rect?.top,
|
||||
left: state.rect?.left,
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
import { type InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
export interface VarTreeNode extends TreeNodeData {
|
||||
varInputType?: InputType;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
export const studioVarTextareaLineHeightKey =
|
||||
'--studio-var-textarea-line-height';
|
||||
|
||||
export const studioVarTextareaLineHeight = 22;
|
||||
|
||||
export const getCssVarStyle = (options?: {
|
||||
rows?: number;
|
||||
style?: CSSProperties;
|
||||
}): CSSProperties | undefined => {
|
||||
const { rows, style } = options ?? {};
|
||||
|
||||
if (typeof rows !== 'number') {
|
||||
return style;
|
||||
}
|
||||
|
||||
const vars = {
|
||||
[studioVarTextareaLineHeightKey]: studioVarTextareaLineHeight * rows,
|
||||
};
|
||||
|
||||
return {
|
||||
...style,
|
||||
...vars,
|
||||
};
|
||||
};
|
||||
|
||||
export const componentTypeOptionMap: Partial<
|
||||
Record<
|
||||
InputType,
|
||||
{
|
||||
label: string;
|
||||
}
|
||||
>
|
||||
> = {
|
||||
[InputType.TextInput]: {
|
||||
label: I18n.t('shortcut_component_type_text'),
|
||||
},
|
||||
[InputType.Select]: {
|
||||
label: I18n.t('shortcut_component_type_selector'),
|
||||
},
|
||||
[InputType.MixUpload]: {
|
||||
label: I18n.t('shortcut_modal_components_modal_upload_component'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type FC } from 'react';
|
||||
|
||||
export const InputTypeTag: FC<PropsWithChildren> = ({ children }) => (
|
||||
<span className="coz-mg-secondary-hovered rounded-[4px] h-[16px] text-[12px] coz-fg-primary px-[5px] leading-[16px]">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const VarListItem: FC<PropsWithChildren> = ({ children }) => (
|
||||
<div className="flex justify-between items-center px-[4px] text-[14px] font-normal coz-fg-primary">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||