Browse Source

feat: support prompt messages sorting (#3757)

Joel 1 year ago
parent
commit
1ad70f8721

File diff suppressed because it is too large
+ 5 - 0
web/app/components/base/icons/assets/vender/line/others/drag-handle.svg


File diff suppressed because it is too large
+ 38 - 0
web/app/components/base/icons/src/vender/line/others/DragHandle.json


+ 16 - 0
web/app/components/base/icons/src/vender/line/others/DragHandle.tsx

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

+ 1 - 0
web/app/components/base/icons/src/vender/line/others/index.ts

@@ -0,0 +1 @@
+export { default as DragHandle } from './DragHandle'

+ 6 - 2
web/app/components/workflow/nodes/_base/components/prompt/editor.tsx

@@ -22,6 +22,8 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
 import TooltipPlus from '@/app/components/base/tooltip-plus'
 
 type Props = {
+  className?: string
+  headerClassName?: string
   instanceId?: string
   title: string | JSX.Element
   value: string
@@ -43,6 +45,8 @@ type Props = {
 }
 
 const Editor: FC<Props> = ({
+  className,
+  headerClassName,
   instanceId,
   title,
   value,
@@ -102,10 +106,10 @@ const Editor: FC<Props> = ({
   }
 
   return (
-    <div className={cn(wrapClassName)} style={wrapStyle}>
+    <div className={cn(className, wrapClassName)} style={wrapStyle}>
       <div ref={ref} className={cn(isFocus ? s.gradientBorder : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5')}>
         <div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
-          <div className='pt-1 pl-3 pr-2 flex justify-between h-6 items-center'>
+          <div className={cn(headerClassName, 'pt-1 pl-3 pr-2 flex justify-between h-6 items-center')}>
             <div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
             <div className='flex items-center'>
               <div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div>

+ 3 - 1
web/app/components/workflow/nodes/_base/components/selector.tsx

@@ -14,6 +14,7 @@ type Props = {
   DropDownIcon?: any
   noLeft?: boolean
   options: Item[]
+  allOptions?: Item[]
   value: string
   placeholder?: string
   onChange: (value: any) => void
@@ -30,6 +31,7 @@ const TypeSelector: FC<Props> = ({
   DropDownIcon = ChevronSelectorVertical,
   noLeft,
   options: list,
+  allOptions,
   value,
   placeholder = '',
   onChange,
@@ -41,7 +43,7 @@ const TypeSelector: FC<Props> = ({
   showChecked,
 }) => {
   const noValue = value === '' || value === undefined || value === null
-  const item = list.find(item => item.value === value)
+  const item = allOptions ? allOptions.find(item => item.value === value) : list.find(item => item.value === value)
   const [showOption, { setFalse: setHide, toggle: toggleShow }] = useBoolean(false)
   const ref = React.useRef(null)
   useClickAway(() => {

+ 25 - 8
web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx

@@ -13,6 +13,9 @@ import { PromptRole } from '@/models/debug'
 const i18nPrefix = 'workflow.nodes.llm'
 
 type Props = {
+  className?: string
+  headerClassName?: string
+  canNotChooseSystemRole?: boolean
   readOnly: boolean
   id: string
   canRemove: boolean
@@ -47,7 +50,12 @@ const roleOptions = [
   },
 ]
 
+const roleOptionsWithoutSystemRole = roleOptions.filter(item => item.value !== PromptRole.system)
+
 const ConfigPromptItem: FC<Props> = ({
+  className,
+  headerClassName,
+  canNotChooseSystemRole,
   readOnly,
   id,
   canRemove,
@@ -68,19 +76,28 @@ const ConfigPromptItem: FC<Props> = ({
     setInstanceId(`${id}-${uniqueId()}`)
   }, [id])
   return (
-
     <Editor
+      className={className}
+      headerClassName={headerClassName}
       instanceId={instanceId}
       key={instanceId}
       title={
         <div className='relative left-1 flex items-center'>
-          <TypeSelector
-            value={payload.role as string}
-            options={roleOptions}
-            onChange={handleChatModeMessageRoleChange}
-            triggerClassName='text-xs font-semibold text-gray-700 uppercase'
-            itemClassName='text-[13px] font-medium text-gray-700'
-          />
+          {payload.role === PromptRole.system
+            ? (<div className='relative left-[-4px] text-xs font-semibold text-gray-700 uppercase'>
+              SYSTEM
+            </div>)
+            : (
+              <TypeSelector
+                value={payload.role as string}
+                allOptions={roleOptions}
+                options={canNotChooseSystemRole ? roleOptionsWithoutSystemRole : roleOptions}
+                onChange={handleChatModeMessageRoleChange}
+                triggerClassName='text-xs font-semibold text-gray-700 uppercase'
+                itemClassName='text-[13px] font-medium text-gray-700'
+              />
+            )}
+
           <TooltipPlus
             popupContent={
               <div className='max-w-[180px]'>{t(`${i18nPrefix}.roleDescription.${payload.role}`)}</div>

+ 73 - 24
web/app/components/workflow/nodes/llm/components/config-prompt.tsx

@@ -3,12 +3,17 @@ import type { FC } from 'react'
 import React, { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
 import produce from 'immer'
+import { ReactSortable } from 'react-sortablejs'
+import { v4 as uuid4 } from 'uuid'
+import cn from 'classnames'
 import type { PromptItem, ValueSelector, Var } from '../../../types'
 import { PromptRole } from '../../../types'
 import useAvailableVarList from '../../_base/hooks/use-available-var-list'
 import ConfigPromptItem from './config-prompt-item'
 import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
 import AddButton from '@/app/components/workflow/nodes/_base/components/add-button'
+import { DragHandle } from '@/app/components/base/icons/src/vender/line/others'
+
 const i18nPrefix = 'workflow.nodes.llm'
 
 type Props = {
@@ -39,6 +44,18 @@ const ConfigPrompt: FC<Props> = ({
   hasSetBlockStatus,
 }) => {
   const { t } = useTranslation()
+  const payloadWithIds = (isChatModel && Array.isArray(payload))
+    ? payload.map((item, i) => {
+      const id = uuid4()
+      return {
+        id: item.id || id,
+        p: {
+          ...item,
+          id: item.id || id,
+        },
+      }
+    })
+    : []
   const {
     availableVars,
     availableNodes,
@@ -94,37 +111,69 @@ const ConfigPrompt: FC<Props> = ({
     onChange(newPrompt)
   }, [onChange, payload])
 
-  // console.log(getInputVars((payload as PromptItem).text))
+  const canChooseSystemRole = (() => {
+    if (isChatModel && Array.isArray(payload))
+      return !payload.find(item => item.role === PromptRole.system)
 
+    return false
+  })()
   return (
     <div>
       {(isChatModel && Array.isArray(payload))
         ? (
           <div>
             <div className='space-y-2'>
-              {
-                (payload as PromptItem[]).map((item, index) => {
-                  return (
-                    <ConfigPromptItem
-                      key={`${payload.length}-${index}`}
-                      canRemove={payload.length > 1}
-                      readOnly={readOnly}
-                      id={`${payload.length}-${index}`}
-                      handleChatModeMessageRoleChange={handleChatModeMessageRoleChange(index)}
-                      isChatModel={isChatModel}
-                      isChatApp={isChatApp}
-                      payload={item}
-                      onPromptChange={handleChatModePromptChange(index)}
-                      onRemove={handleRemove(index)}
-                      isShowContext={isShowContext}
-                      hasSetBlockStatus={hasSetBlockStatus}
-                      availableVars={availableVars}
-                      availableNodes={availableNodes}
-                    />
-                  )
-                })
-
-              }
+              <ReactSortable className="space-y-1"
+                list={payloadWithIds}
+                setList={(list) => {
+                  if ((payload as PromptItem[])?.[0].role === PromptRole.system && list[0].p.role !== PromptRole.system)
+                    return
+
+                  onChange(list.map(item => item.p))
+                }}
+                handle='.handle'
+                ghostClass="opacity-50"
+                animation={150}
+              >
+                {
+                  (payload as PromptItem[]).map((item, index) => {
+                    const canDrag = (() => {
+                      if (readOnly)
+                        return false
+
+                      if (index === 0 && item.role === PromptRole.system)
+                        return false
+
+                      return true
+                    })()
+                    return (
+                      <div key={item.id} className='relative group'>
+                        {canDrag && <DragHandle className='group-hover:block hidden absolute left-[-14px] top-2 w-3.5 h-3.5 text-gray-400' />}
+                        <ConfigPromptItem
+                          className={cn(canDrag && 'handle')}
+                          headerClassName={cn(canDrag && 'cursor-grab')}
+                          canNotChooseSystemRole={!canChooseSystemRole}
+                          canRemove={payload.length > 1 && !(index === 0 && item.role === PromptRole.system)}
+                          readOnly={readOnly}
+                          id={item.id!}
+                          handleChatModeMessageRoleChange={handleChatModeMessageRoleChange(index)}
+                          isChatModel={isChatModel}
+                          isChatApp={isChatApp}
+                          payload={item}
+                          onPromptChange={handleChatModePromptChange(index)}
+                          onRemove={handleRemove(index)}
+                          isShowContext={isShowContext}
+                          hasSetBlockStatus={hasSetBlockStatus}
+                          availableVars={availableVars}
+                          availableNodes={availableNodes}
+                        />
+                      </div>
+
+                    )
+                  })
+
+                }
+              </ReactSortable>
             </div>
             <AddButton
               className='mt-2'

+ 1 - 0
web/app/components/workflow/types.ts

@@ -122,6 +122,7 @@ export enum PromptRole {
 }
 
 export type PromptItem = {
+  id?: string
   role?: PromptRole
   text: string
 }

File diff suppressed because it is too large
+ 974 - 586
web/yarn.lock