Explorar el Código

feat: support optional query content (#1097)

Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
Joel hace 1 año
padre
commit
2d5ad0d208
Se han modificado 36 ficheros con 548 adiciones y 290 borrados
  1. 106 3
      api/commands.py
  2. 12 1
      api/constants/model_template.py
  3. 1 1
      api/controllers/console/app/completion.py
  4. 1 1
      api/controllers/console/explore/completion.py
  5. 1 1
      api/controllers/service_api/app/completion.py
  6. 1 1
      api/controllers/web/completion.py
  7. 1 1
      api/core/model_providers/models/llm/base.py
  8. 1 1
      api/core/prompt/generate_prompts/baichuan_chat.json
  9. 1 1
      api/core/prompt/generate_prompts/common_chat.json
  10. 2 2
      api/services/app_model_config_service.py
  11. 3 3
      api/services/completion_service.py
  12. 19 14
      web/app/components/app/configuration/config-var/config-model/index.tsx
  13. 0 0
      web/app/components/app/configuration/config-var/config-modal/style.module.css
  14. 14 2
      web/app/components/app/configuration/config-var/config-string/index.tsx
  15. 2 2
      web/app/components/app/configuration/config-var/index.tsx
  16. 8 0
      web/app/components/app/configuration/config-var/input-type-icon.tsx
  17. 56 26
      web/app/components/app/configuration/config-var/select-type-item/index.tsx
  18. 4 3
      web/app/components/app/configuration/config-var/select-type-item/style.module.css
  19. 0 9
      web/app/components/app/configuration/debug/index.tsx
  20. 8 1
      web/app/components/app/configuration/index.tsx
  21. 90 77
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  22. 50 0
      web/app/components/base/tooltip-plus/index.tsx
  23. 1 5
      web/app/components/develop/template/template.en.mdx
  24. 1 5
      web/app/components/develop/template/template.zh.mdx
  25. 20 12
      web/app/components/share/chat/welcome/index.tsx
  26. 21 12
      web/app/components/share/chatbot/welcome/index.tsx
  27. 20 22
      web/app/components/share/text-generation/index.tsx
  28. 0 8
      web/app/components/share/text-generation/result/index.tsx
  29. 1 1
      web/app/components/share/text-generation/run-batch/csv-download/index.tsx
  30. 55 54
      web/app/components/share/text-generation/run-once/index.tsx
  31. 5 0
      web/config/index.ts
  32. 5 1
      web/i18n/lang/app-debug.en.ts
  33. 4 0
      web/i18n/lang/app-debug.zh.ts
  34. 2 1
      web/i18n/lang/share-app.en.ts
  35. 3 1
      web/i18n/lang/share-app.zh.ts
  36. 29 18
      web/utils/model-config.ts

+ 106 - 3
api/commands.py

@@ -6,6 +6,7 @@ import string
 import time
 
 import click
+from tqdm import tqdm
 from flask import current_app
 from langchain.embeddings import OpenAIEmbeddings
 from werkzeug.exceptions import NotFound
@@ -21,9 +22,9 @@ from libs.password import password_pattern, valid_password, hash_password
 from libs.helper import email as email_validate
 from extensions.ext_database import db
 from libs.rsa import generate_key_pair
-from models.account import InvitationCode, Tenant
+from models.account import InvitationCode, Tenant, TenantAccountJoin
 from models.dataset import Dataset, DatasetQuery, Document
-from models.model import Account
+from models.model import Account, AppModelConfig, App
 import secrets
 import base64
 
@@ -439,6 +440,107 @@ def update_qdrant_indexes():
 
     click.echo(click.style('Congratulations! Update {} dataset indexes.'.format(create_count), fg='green'))
 
+@click.command('update_app_model_configs', help='Migrate data to support paragraph variable.')
+@click.option("--batch-size", default=500, help="Number of records to migrate in each batch.")
+def update_app_model_configs(batch_size):
+    pre_prompt_template = '{{default_input}}'
+    user_input_form_template = {
+        "en-US": [
+            {
+                "paragraph": {
+                    "label": "Query",
+                    "variable": "default_input",
+                    "required": False,
+                    "default": ""
+                }
+            }
+        ],
+        "zh-Hans": [
+            {
+                "paragraph": {
+                    "label": "查询内容",
+                    "variable": "default_input",
+                    "required": False,
+                    "default": ""
+                }
+            }
+        ]
+    }
+
+    click.secho("Start migrate old data that the text generator can support paragraph variable.", fg='green')
+
+    total_records = db.session.query(AppModelConfig) \
+        .join(App, App.app_model_config_id == AppModelConfig.id) \
+        .filter(App.mode == 'completion') \
+        .count()
+    
+    if total_records == 0:
+        click.secho("No data to migrate.", fg='green')
+        return
+
+    num_batches = (total_records + batch_size - 1) // batch_size
+
+    with tqdm(total=total_records, desc="Migrating Data") as pbar:
+        for i in range(num_batches):
+            offset = i * batch_size
+            limit = min(batch_size, total_records - offset)
+
+            click.secho(f"Fetching batch {i+1}/{num_batches} from source database...", fg='green')
+            
+            data_batch = db.session.query(AppModelConfig) \
+                .join(App, App.app_model_config_id == AppModelConfig.id) \
+                .filter(App.mode == 'completion') \
+                .order_by(App.created_at) \
+                .offset(offset).limit(limit).all()
+            
+            if not data_batch:
+                click.secho("No more data to migrate.", fg='green')
+                break
+
+            try:
+                click.secho(f"Migrating {len(data_batch)} records...", fg='green')
+                for data in data_batch:
+                    # click.secho(f"Migrating data {data.id}, pre_prompt: {data.pre_prompt}, user_input_form: {data.user_input_form}", fg='green')
+
+                    if data.pre_prompt is None:
+                        data.pre_prompt = pre_prompt_template
+                    else:
+                        if pre_prompt_template in data.pre_prompt:
+                            continue
+                        data.pre_prompt += pre_prompt_template
+
+                    app_data = db.session.query(App) \
+                        .filter(App.id == data.app_id) \
+                        .one()
+                    
+                    account_data = db.session.query(Account) \
+                        .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) \
+                        .filter(TenantAccountJoin.role == 'owner') \
+                        .filter(TenantAccountJoin.tenant_id == app_data.tenant_id) \
+                        .one_or_none()
+
+                    if not account_data:
+                        continue
+
+                    if data.user_input_form is None or data.user_input_form == 'null':
+                        data.user_input_form = json.dumps(user_input_form_template[account_data.interface_language])
+                    else:
+                        raw_json_data = json.loads(data.user_input_form)
+                        raw_json_data.append(user_input_form_template[account_data.interface_language][0])
+                        data.user_input_form = json.dumps(raw_json_data)
+
+                    # click.secho(f"Updated data {data.id}, pre_prompt: {data.pre_prompt}, user_input_form: {data.user_input_form}", fg='green')
+
+                db.session.commit()
+
+            except Exception as e:
+                click.secho(f"Error while migrating data: {e}, app_id: {data.app_id}, app_model_config_id: {data.id}", fg='red')
+                continue
+            
+            click.secho(f"Successfully migrated batch {i+1}/{num_batches}.", fg='green')
+            
+            pbar.update(len(data_batch))
+
 def register_commands(app):
     app.cli.add_command(reset_password)
     app.cli.add_command(reset_email)
@@ -448,4 +550,5 @@ def register_commands(app):
     app.cli.add_command(sync_anthropic_hosted_providers)
     app.cli.add_command(clean_unused_dataset_indexes)
     app.cli.add_command(create_qdrant_indexes)
-    app.cli.add_command(update_qdrant_indexes)
+    app.cli.add_command(update_qdrant_indexes)
+    app.cli.add_command(update_app_model_configs)

+ 12 - 1
api/constants/model_template.py

@@ -38,7 +38,18 @@ model_templates = {
                     "presence_penalty": 0,
                     "frequency_penalty": 0
                 }
-            })
+            }),
+            'user_input_form': json.dumps([
+                {
+                    "paragraph": {
+                        "label": "Query",
+                        "variable": "query",
+                        "required": True,
+                        "default": ""
+                    }
+                }
+            ]),
+            'pre_prompt': '{{query}}'
         }
     },
 

+ 1 - 1
api/controllers/console/app/completion.py

@@ -39,7 +39,7 @@ class CompletionMessageApi(Resource):
 
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, required=True, location='json')
-        parser.add_argument('query', type=str, location='json')
+        parser.add_argument('query', type=str, location='json', default='')
         parser.add_argument('model_config', type=dict, required=True, location='json')
         parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
         args = parser.parse_args()

+ 1 - 1
api/controllers/console/explore/completion.py

@@ -31,7 +31,7 @@ class CompletionApi(InstalledAppResource):
 
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, required=True, location='json')
-        parser.add_argument('query', type=str, location='json')
+        parser.add_argument('query', type=str, location='json', default='')
         parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
         args = parser.parse_args()
 

+ 1 - 1
api/controllers/service_api/app/completion.py

@@ -27,7 +27,7 @@ class CompletionApi(AppApiResource):
 
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, required=True, location='json')
-        parser.add_argument('query', type=str, location='json')
+        parser.add_argument('query', type=str, location='json', default='')
         parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
         parser.add_argument('user', type=str, location='json')
         args = parser.parse_args()

+ 1 - 1
api/controllers/web/completion.py

@@ -29,7 +29,7 @@ class CompletionApi(WebApiResource):
 
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, required=True, location='json')
-        parser.add_argument('query', type=str, location='json')
+        parser.add_argument('query', type=str, location='json', default='')
         parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
         args = parser.parse_args()
 

+ 1 - 1
api/core/model_providers/models/llm/base.py

@@ -342,7 +342,7 @@ class BaseLLM(BaseProviderModel):
             if order == 'context_prompt':
                 prompt += context_prompt_content
             elif order == 'pre_prompt':
-                prompt += (pre_prompt_content + '\n\n') if pre_prompt_content else ''
+                prompt += pre_prompt_content
 
         query_prompt = prompt_rules['query_prompt'] if 'query_prompt' in prompt_rules else '{{query}}'
 

+ 1 - 1
api/core/prompt/generate_prompts/baichuan_chat.json

@@ -8,6 +8,6 @@
     "pre_prompt",
     "histories_prompt"
   ],
-  "query_prompt": "用户:{{query}}",
+  "query_prompt": "\n\n用户:{{query}}",
   "stops": ["用户:"]
 }

+ 1 - 1
api/core/prompt/generate_prompts/common_chat.json

@@ -8,6 +8,6 @@
     "pre_prompt",
     "histories_prompt"
   ],
-  "query_prompt": "Human: {{query}}\n\nAssistant: ",
+  "query_prompt": "\n\nHuman: {{query}}\n\nAssistant: ",
   "stops": ["\nHuman:", "</histories>"]
 }

+ 2 - 2
api/services/app_model_config_service.py

@@ -216,8 +216,8 @@ class AppModelConfigService:
         variables = []
         for item in config["user_input_form"]:
             key = list(item.keys())[0]
-            if key not in ["text-input", "select"]:
-                raise ValueError("Keys in user_input_form list can only be 'text-input' or 'select'")
+            if key not in ["text-input", "select", "paragraph"]:
+                raise ValueError("Keys in user_input_form list can only be 'text-input', 'paragraph'  or 'select'")
 
             form_item = item[key]
             if 'label' not in form_item:

+ 3 - 3
api/services/completion_service.py

@@ -34,7 +34,7 @@ class CompletionService:
         inputs = args['inputs']
         query = args['query']
 
-        if not query:
+        if app_model.mode != 'completion' and not query:
             raise ValueError('query is required')
 
         query = query.replace('\x00', '')
@@ -347,8 +347,8 @@ class CompletionService:
                 if value not in options:
                     raise ValueError(f"{variable} in input form must be one of the following: {options}")
             else:
-                if 'max_length' in variable:
-                    max_length = variable['max_length']
+                if 'max_length' in input_config:
+                    max_length = input_config['max_length']
                     if len(value) > max_length:
                         raise ValueError(f'{variable} in input form must be less than {max_length} characters')
 

+ 19 - 14
web/app/components/app/configuration/config-var/config-model/index.tsx

@@ -2,6 +2,7 @@
 import type { FC } from 'react'
 import React, { useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
 import ModalFoot from '../modal-foot'
 import type { Options } from '../config-select'
 import ConfigSelect from '../config-select'
@@ -11,6 +12,7 @@ import s from './style.module.css'
 import Toast from '@/app/components/base/toast'
 import type { PromptVariable } from '@/models/debug'
 import { getNewVar } from '@/utils/var'
+import ConfigContext from '@/context/debug-configuration'
 
 import Modal from '@/app/components/base/modal'
 
@@ -28,6 +30,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
   onClose,
   onConfirm,
 }) => {
+  const { modelConfig } = useContext(ConfigContext)
   const { t } = useTranslation()
   const { type, name, key, options, max_length } = payload || getNewVar('')
 
@@ -41,7 +44,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
     }
   }
 
-  const isStringInput = tempType === 'string'
+  const isStringInput = tempType === 'string' || tempType === 'paragraph'
   const title = isStringInput ? t('appDebug.variableConig.maxLength') : t('appDebug.variableConig.options')
 
   // string type
@@ -93,22 +96,24 @@ const ConfigModal: FC<IConfigModalProps> = ({
         <div className='mb-2'>
           <div className={s.title}>{t('appDebug.variableConig.fieldType')}</div>
           <div className='flex space-x-2'>
-            <SelectTypeItem type='string' selected={isStringInput} onClick={handleTypeChange('string')} />
-            <SelectTypeItem type='select' selected={!isStringInput} onClick={handleTypeChange('select')} />
+            <SelectTypeItem type='string' selected={tempType === 'string'} onClick={handleTypeChange('string')} />
+            <SelectTypeItem type='paragraph' selected={tempType === 'paragraph'} onClick={handleTypeChange('paragraph')} />
+            <SelectTypeItem type='select' selected={tempType === 'select'} onClick={handleTypeChange('select')} />
           </div>
         </div>
 
-        <div className='mt-6'>
-          <div className={s.title}>{title}</div>
-          {isStringInput
-            ? (
-              <ConfigString value={tempMaxLength} onChange={setTempMaxValue} />
-            )
-            : (
-              <ConfigSelect options={tempOptions} onChange={setTempOptions} />
-            )}
-        </div>
-
+        {tempType !== 'paragraph' && (
+          <div className='mt-6'>
+            <div className={s.title}>{title}</div>
+            {isStringInput
+              ? (
+                <ConfigString modelId={modelConfig.model_id} value={tempMaxLength} onChange={setTempMaxValue} />
+              )
+              : (
+                <ConfigSelect options={tempOptions} onChange={setTempOptions} />
+              )}
+          </div>
+        )}
       </div>
       <ModalFoot
         onConfirm={handleConfirm}

web/app/components/app/configuration/config-var/config-model/style.module.css → web/app/components/app/configuration/config-var/config-modal/style.module.css


+ 14 - 2
web/app/components/app/configuration/config-var/config-string/index.tsx

@@ -1,9 +1,10 @@
 'use client'
 import type { FC } from 'react'
-import React from 'react'
+import React, { useEffect } from 'react'
 
 export type IConfigStringProps = {
   value: number | undefined
+  modelId: string
   onChange: (value: number | undefined) => void
 }
 
@@ -13,6 +14,11 @@ const ConfigString: FC<IConfigStringProps> = ({
   value,
   onChange,
 }) => {
+  useEffect(() => {
+    if (value && value > MAX_LENGTH)
+      onChange(MAX_LENGTH)
+  }, [value, MAX_LENGTH])
+
   return (
     <div>
       <input
@@ -21,7 +27,13 @@ const ConfigString: FC<IConfigStringProps> = ({
         min={1}
         value={value || ''}
         onChange={(e) => {
-          const value = Math.max(1, Math.min(MAX_LENGTH, parseInt(e.target.value))) || 1
+          let value = parseInt(e.target.value, 10)
+          if (value > MAX_LENGTH)
+            value = MAX_LENGTH
+
+          else if (value < 1)
+            value = 1
+
           onChange(value)
         }}
         className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"

+ 2 - 2
web/app/components/app/configuration/config-var/index.tsx

@@ -8,7 +8,7 @@ import type { Timeout } from 'ahooks/lib/useRequest/src/types'
 import Panel from '../base/feature-panel'
 import OperationBtn from '../base/operation-btn'
 import VarIcon from '../base/icons/var-icon'
-import EditModel from './config-model'
+import EditModal from './config-modal'
 import IconTypeIcon from './input-type-icon'
 import type { IInputTypeIconProps } from './input-type-icon'
 import s from './style.module.css'
@@ -240,7 +240,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
       )}
 
       {isShowEditModal && (
-        <EditModel
+        <EditModal
           payload={currItem as PromptVariable}
           isShow={isShowEditModal}
           onClose={hideEditModal}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 8 - 0
web/app/components/app/configuration/config-var/input-type-icon.tsx


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 56 - 26
web/app/components/app/configuration/config-var/select-type-item/index.tsx


+ 4 - 3
web/app/components/app/configuration/config-var/select-type-item/style.module.css

@@ -1,9 +1,10 @@
 .item {
   display: flex;
+  flex-direction: column;
+  justify-content: center;
   align-items: center;
-  height: 32px;
-  width: 133px;
-  padding-left: 12px;
+  height: 58px;
+  width: 98px;
   border-radius: 8px;
   border: 1px solid #EAECF0;
   box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);

+ 0 - 9
web/app/components/app/configuration/debug/index.tsx

@@ -297,7 +297,6 @@ const Debug: FC<IDebug> = ({
       setChatList([])
   }, [controlClearChatMessage])
 
-  const [completionQuery, setCompletionQuery] = useState('')
   const [completionRes, setCompletionRes] = useState('')
 
   const sendTextCompletion = async () => {
@@ -309,11 +308,6 @@ const Debug: FC<IDebug> = ({
     if (!checkCanSend())
       return
 
-    if (!completionQuery) {
-      logError(t('appDebug.errorMessage.queryRequired'))
-      return false
-    }
-
     const postDatasets = dataSets.map(({ id }) => ({
       dataset: {
         enabled: true,
@@ -342,7 +336,6 @@ const Debug: FC<IDebug> = ({
 
     const data = {
       inputs,
-      query: completionQuery,
       model_config: postModelConfig,
     }
 
@@ -380,8 +373,6 @@ const Debug: FC<IDebug> = ({
         </div>
         <PromptValuePanel
           appType={mode as AppType}
-          value={completionQuery}
-          onChange={setCompletionQuery}
           onSend={sendTextCompletion}
         />
       </div>

+ 8 - 1
web/app/components/app/configuration/index.tsx

@@ -6,6 +6,7 @@ import { useContext } from 'use-context-selector'
 import { usePathname } from 'next/navigation'
 import produce from 'immer'
 import { useBoolean } from 'ahooks'
+import cn from 'classnames'
 import Button from '../../base/button'
 import Loading from '../../base/loading'
 import type { CompletionParams, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptVariable } from '@/models/debug'
@@ -24,6 +25,7 @@ import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from
 import { fetchDatasets } from '@/service/datasets'
 import AccountSetting from '@/app/components/header/account-setting'
 import { useProviderContext } from '@/context/provider-context'
+import { AppType } from '@/types/app'
 
 const Configuration: FC = () => {
   const { t } = useTranslation()
@@ -193,11 +195,16 @@ const Configuration: FC = () => {
     })
   }, [appId])
 
+  const cannotPublish = mode === AppType.completion && !modelConfig.configs.prompt_template
   const saveAppConfig = async () => {
     const modelId = modelConfig.model_id
     const promptTemplate = modelConfig.configs.prompt_template
     const promptVariables = modelConfig.configs.prompt_variables
 
+    if (cannotPublish) {
+      notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty'), duration: 3000 })
+      return
+    }
     const postDatasets = dataSets.map(({ id }) => ({
       dataset: {
         enabled: true,
@@ -311,7 +318,7 @@ const Configuration: FC = () => {
               />
               <div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
               <Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
-              <Button type='primary' onClick={saveAppConfig} className='shrink-0 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.applyConfig')}</Button>
+              <Button type='primary' onClick={saveAppConfig} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
             </div>
           </div>
           <div className='flex grow h-[200px]'>

+ 90 - 77
web/app/components/app/configuration/prompt-value-panel/index.tsx

@@ -14,11 +14,10 @@ import Select from '@/app/components/base/select'
 import { DEFAULT_VALUE_MAX_LEN } from '@/config'
 import Button from '@/app/components/base/button'
 import { ChevronDown, ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
+import Tooltip from '@/app/components/base/tooltip-plus'
 
 export type IPromptValuePanelProps = {
   appType: AppType
-  value?: string
-  onChange?: (value: string) => void
   onSend?: () => void
 }
 
@@ -32,12 +31,10 @@ const starIcon = (
 
 const PromptValuePanel: FC<IPromptValuePanelProps> = ({
   appType,
-  value,
-  onChange,
   onSend,
 }) => {
   const { t } = useTranslation()
-  const { modelConfig, inputs, setInputs } = useContext(ConfigContext)
+  const { modelConfig, inputs, setInputs, mode } = useContext(ConfigContext)
   const [promptPreviewCollapse, setPromptPreviewCollapse] = useState(false)
   const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
   const promptTemplate = modelConfig.configs.prompt_template
@@ -53,6 +50,19 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     return obj
   })()
 
+  const canNotRun = mode === AppType.completion && !modelConfig.configs.prompt_template
+  const renderRunButton = () => {
+    return (
+      <Button
+        type="primary"
+        disabled={canNotRun}
+        onClick={() => onSend && onSend()}
+        className="w-[80px] !h-8">
+        <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
+        <span className='uppercase text-[13px]'>{t('appDebug.inputs.run')}</span>
+      </Button>
+    )
+  }
   const handleInputValueChange = (key: string, value: string) => {
     if (!(key in promptVariableObj))
       return
@@ -65,6 +75,14 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     setInputs(newInputs)
   }
 
+  const onClear = () => {
+    const newInputs: Record<string, any> = {}
+    promptVariables.forEach((item) => {
+      newInputs[item.key] = ''
+    })
+    setInputs(newInputs)
+  }
+
   const promptPreview = (
     <div className='py-3 rounded-t-xl bg-indigo-25'>
       <div className="px-4">
@@ -125,83 +143,78 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
             <div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
           )}
         </div>
-        {
-          !userInputFieldCollapse && (
-            <>
-              {
-                promptVariables.length > 0
-                  ? (
-                    <div className="space-y-3 ">
-                      {promptVariables.map(({ key, name, type, options, max_length, required }) => (
-                        <div key={key} className="flex items-center justify-between">
-                          <div className="mr-1 shrink-0 w-[120px] text-sm text-gray-900 break-all">{name || key}</div>
-                          {type === 'select'
-                            ? (
-                              <Select
-                                className='w-full'
-                                defaultValue={inputs[key] as string}
-                                onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
-                                items={(options || []).map(i => ({ name: i, value: i }))}
-                                allowSearch={false}
-                                bgClassName='bg-gray-50'
-                                overlayClassName='z-[11]'
-                              />
-                            )
-                            : (
-                              <input
-                                className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
-                                placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                                type="text"
-                                value={inputs[key] ? `${inputs[key]}` : ''}
-                                onChange={(e) => { handleInputValueChange(key, e.target.value) }}
-                                maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
-                              />
-                            )}
-
-                        </div>
-                      ))}
-                    </div>
-                  )
-                  : (
-                    <div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
-                  )
-              }
-            </>
-          )
+        {!userInputFieldCollapse && (
+          <>
+            {
+              promptVariables.length > 0
+                ? (
+                  <div className="space-y-3 ">
+                    {promptVariables.map(({ key, name, type, options, max_length, required }) => (
+                      <div key={key} className="flex justify-between">
+                        <div className="mr-1 pt-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
+                        {type === 'select' && (
+                          <Select
+                            className='w-full'
+                            defaultValue={inputs[key] as string}
+                            onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
+                            items={(options || []).map(i => ({ name: i, value: i }))}
+                            allowSearch={false}
+                            bgClassName='bg-gray-50'
+                          />
+                        )
+                        }
+                        {type === 'string' && (
+                          <input
+                            className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
+                            placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                            type="text"
+                            value={inputs[key] ? `${inputs[key]}` : ''}
+                            onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                            maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
+                          />
+                        )}
+                        {type === 'paragraph' && (
+                          <textarea
+                            className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-[120px] bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
+                            placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                            value={inputs[key] ? `${inputs[key]}` : ''}
+                            onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                          />
+                        )}
+
+                      </div>
+                    ))}
+                  </div>
+                )
+                : (
+                  <div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
+                )
+            }
+          </>
+        )
         }
       </div>
 
       {
         appType === AppType.completion && (
-          <div className='px-4'>
-            <div className="mt-3 border-b border-gray-100"></div>
-            <div className="mt-4">
-              <div>
-                <div className="text-[13px] text-gray-900 font-medium">{t('appDebug.inputs.queryTitle')}</div>
-                <div className="mt-2 mb-4 overflow-hidden border border-gray-200 rounded-lg grow bg-gray-50 ">
-                  <div className="px-4 py-2 rounded-t-lg bg-gray-50">
-                    <textarea
-                      rows={4}
-                      className="w-full px-0 text-sm text-gray-900 border-0 bg-gray-50 focus:outline-none placeholder:bg-gray-50"
-                      placeholder={t('appDebug.inputs.queryPlaceholder') as string}
-                      value={value}
-                      onChange={e => onChange && onChange(e.target.value)}
-                    ></textarea>
-                  </div>
-                  <div className="flex items-center justify-between px-3 py-2 bg-gray-50">
-                    <div className="flex pl-0 space-x-1 sm:pl-2">
-                      <span className="bg-gray-100 text-gray-500 text-xs font-medium mr-2 px-2.5 py-0.5 rounded cursor-pointer">{value?.length}</span>
-                    </div>
-                    <Button
-                      type="primary"
-                      onClick={() => onSend && onSend()}
-                      className="w-[80px] !h-8">
-                      <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
-                      <span className='uppercase text-[13px]'>{t('appDebug.inputs.run')}</span>
-                    </Button>
-                  </div>
-                </div>
-              </div>
+          <div>
+            <div className="mt-5 border-b border-gray-100"></div>
+            <div className="flex justify-between mt-4 px-4">
+              <Button
+                className='!h-8 !p-3'
+                onClick={onClear}
+                disabled={false}
+              >
+                <span className='text-[13px]'>{t('common.operation.clear')}</span>
+              </Button>
+
+              {canNotRun
+                ? (<Tooltip
+                  popupContent={t('appDebug.otherError.promptNoBeEmpty')}
+                >
+                  {renderRunButton()}
+                </Tooltip>)
+                : renderRunButton()}
             </div>
           </div>
         )

+ 50 - 0
web/app/components/base/tooltip-plus/index.tsx

@@ -0,0 +1,50 @@
+'use client'
+import type { FC } from 'react'
+import React, { useState } from 'react'
+import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
+export type TooltipProps = {
+  position?: 'top' | 'right' | 'bottom' | 'left'
+  triggerMethod?: 'hover' | 'click'
+  popupContent: React.ReactNode
+  children: React.ReactNode
+}
+
+const arrow = (
+  <svg className="absolute text-white h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255"><polygon className="fill-current" points="0,0 127.5,127.5 255,0"></polygon></svg>
+)
+
+const Tooltip: FC< TooltipProps> = ({
+  position = 'top',
+  triggerMethod = 'hover',
+  popupContent,
+  children,
+}) => {
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement={position}
+      offset={10}
+    >
+      <PortalToFollowElemTrigger
+        onClick={() => triggerMethod === 'click' && setOpen(v => !v)}
+        onMouseEnter={() => triggerMethod === 'hover' && setOpen(true)}
+        onMouseLeave={() => triggerMethod === 'hover' && setOpen(false)}
+      >
+        {children}
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent
+        className="z-[999]"
+      >
+        <div className='relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg'>
+          {popupContent}
+          {arrow}
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default React.memo(Tooltip)

+ 1 - 5
web/app/components/develop/template/template.en.mdx

@@ -30,9 +30,6 @@ For high-quality text generation, such as articles, summaries, and translations,
         )}
         </ul>
       </Property>
-      <Property name='query' type='string' key='query'>
-        User input text content.
-      </Property>
       <Property name='response_mode' type='string' key='response_mode'>
         - Blocking type, waiting for execution to complete and returning results. (Requests may be interrupted if the process is long)
         - streaming returns. Implementation of streaming return based on SSE (**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**).
@@ -44,7 +41,7 @@ For high-quality text generation, such as articles, summaries, and translations,
   </Col>
   <Col sticky>
 
-    <CodeGroup title="Request" tag="POST" label="/completion-messages" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/completion-messages' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    "inputs": ${JSON.stringify(props.inputs)},\n    "query": "Hi",\n    "response_mode": "streaming"\n    "user": "abc-123"\n}'\n`}>
+    <CodeGroup title="Request" tag="POST" label="/completion-messages" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/completion-messages' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    "inputs": ${JSON.stringify(props.inputs)},\n    "response_mode": "streaming"\n    "user": "abc-123"\n}'\n`}>
 
     ```bash {{ title: 'cURL' }}
     curl --location --request POST 'https://cloud.langgenius.dev/api/completion-messages' \
@@ -52,7 +49,6 @@ For high-quality text generation, such as articles, summaries, and translations,
     --header 'Content-Type: application/json' \
     --data-raw '{
         "inputs": {},
-        "query": "Hi",
         "response_mode": "streaming",
         "user": "abc-123"
     }'

+ 1 - 5
web/app/components/develop/template/template.zh.mdx

@@ -30,9 +30,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
         )}
         </ul>
       </Property>
-      <Property name='query' type='string' key='query'>
-        用户输入的文本正文。
-      </Property>
       <Property name='response_mode' type='string' key='response_mode'>
         - blocking 阻塞型,等待执行完毕后返回结果。(请求若流程较长可能会被中断)
         - streaming 流式返回。基于 SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)实现流式返回。
@@ -44,7 +41,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
   </Col>
   <Col sticky>
 
-    <CodeGroup title="Request" tag="POST" label="/completion-messages" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/completion-messages' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    "inputs": ${JSON.stringify(props.inputs)},\n    "query": "Hi",\n    "response_mode": "streaming",\n    "user": "abc-123"\n}'\n`}>
+    <CodeGroup title="Request" tag="POST" label="/completion-messages" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/completion-messages' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    "inputs": ${JSON.stringify(props.inputs)},\n    "response_mode": "streaming",\n    "user": "abc-123"\n}'\n`}>
 
     ```bash {{ title: 'cURL' }}
     curl --location --request POST 'https://cloud.langgenius.dev/api/completion-messages' \
@@ -52,7 +49,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
     --header 'Content-Type: application/json' \
     --data-raw '{
         "inputs": {},
-        "query": "Hi",
         "response_mode": "streaming",
         "user": "abc-123"
     }'

+ 20 - 12
web/app/components/share/chat/welcome/index.tsx

@@ -97,10 +97,10 @@ const Welcome: FC<IWelcomeProps> = ({
     return (
       <div className='space-y-3'>
         {promptConfig.prompt_variables.map(item => (
-          <div className='tablet:flex tablet:!h-9 mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}>
-            <label className={`flex-shrink-0 flex items-center mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label>
+          <div className='tablet:flex items-start mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}>
+            <label className={`flex-shrink-0 flex items-center tablet:leading-9 mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label>
             {item.type === 'select'
-              ? (
+              && (
                 <Select
                   className='w-full'
                   defaultValue={inputs?.[item.key]}
@@ -109,16 +109,24 @@ const Welcome: FC<IWelcomeProps> = ({
                   allowSearch={false}
                   bgClassName='bg-gray-50'
                 />
-              )
-              : (
-                <input
-                  placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                  value={inputs?.[item.key] || ''}
-                  onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
-                  className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
-                  maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
-                />
               )}
+            {item.type === 'string' && (
+              <input
+                placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                value={inputs?.[item.key] || ''}
+                onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
+                className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
+                maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
+              />
+            )}
+            {item.type === 'paragraph' && (
+              <textarea
+                className="w-full h-[104px] flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50"
+                placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                value={inputs?.[item.key] || ''}
+                onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
+              />
+            )}
           </div>
         ))}
       </div>

+ 21 - 12
web/app/components/share/chatbot/welcome/index.tsx

@@ -97,10 +97,10 @@ const Welcome: FC<IWelcomeProps> = ({
     return (
       <div className='space-y-3'>
         {promptConfig.prompt_variables.map(item => (
-          <div className='tablet:flex tablet:!h-9 mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}>
-            <label className={`flex-shrink-0 flex items-center mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label>
+          <div className='tablet:flex items-start  mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}>
+            <label className={`flex-shrink-0 flex items-center tablet:leading-9 mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label>
             {item.type === 'select'
-              ? (
+              && (
                 <Select
                   className='w-full'
                   defaultValue={inputs?.[item.key]}
@@ -110,15 +110,24 @@ const Welcome: FC<IWelcomeProps> = ({
                   bgClassName='bg-gray-50'
                 />
               )
-              : (
-                <input
-                  placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                  value={inputs?.[item.key] || ''}
-                  onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
-                  className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
-                  maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
-                />
-              )}
+            }
+            {item.type === 'string' && (
+              <input
+                placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                value={inputs?.[item.key] || ''}
+                onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
+                className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
+                maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
+              />
+            )}
+            {item.type === 'paragraph' && (
+              <textarea
+                className="w-full h-[104px] flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50"
+                placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                value={inputs?.[item.key] || ''}
+                onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
+              />
+            )}
           </div>
         ))}
       </div>

+ 20 - 22
web/app/components/share/text-generation/index.tsx

@@ -23,8 +23,9 @@ import { userInputsFormToPromptVariables } from '@/utils/model-config'
 import Res from '@/app/components/share/text-generation/result'
 import SavedItems from '@/app/components/app/text-generate/saved-items'
 import type { InstalledApp } from '@/models/explore'
-import { appDefaultIconBackground } from '@/config'
+import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config'
 import Toast from '@/app/components/base/toast'
+
 const PARALLEL_LIMIT = 5
 enum TaskStatus {
   pending = 'pending',
@@ -34,7 +35,6 @@ enum TaskStatus {
 
 type TaskParam = {
   inputs: Record<string, any>
-  query: string
 }
 
 type Task = {
@@ -65,7 +65,6 @@ const TextGeneration: FC<IMainProps> = ({
   const [isCallBatchAPI, setIsCallBatchAPI] = useState(false)
   const isInBatchTab = currTab === 'batch'
   const [inputs, setInputs] = useState<Record<string, any>>({})
-  const [query, setQuery] = useState('') // run once query content
   const [appId, setAppId] = useState<string>('')
   const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
   const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
@@ -111,11 +110,10 @@ const TextGeneration: FC<IMainProps> = ({
       return {}
     const batchCompletionResLatest = getBatchCompletionRes()
     const res: Record<string, string> = {}
-    const { inputs, query } = task.params
+    const { inputs } = task.params
     promptConfig?.prompt_variables.forEach((v) => {
       res[v.name] = inputs[v.key]
     })
-    res[t('share.generation.queryTitle')] = query
     res[t('share.generation.completionResult')] = batchCompletionResLatest[task.id]
     return res
   })
@@ -135,9 +133,6 @@ const TextGeneration: FC<IMainProps> = ({
         isMapVarName = false
     })
 
-    if (headerData[varLen] !== t('share.generation.queryTitle'))
-      isMapVarName = false
-
     if (!isMapVarName) {
       notify({ type: 'error', message: t('share.generation.errorMsg.fileStructNotMatch') })
       return false
@@ -180,6 +175,8 @@ const TextGeneration: FC<IMainProps> = ({
     }
     let errorRowIndex = 0
     let requiredVarName = ''
+    let moreThanMaxLengthVarName = ''
+    let maxLength = 0
     payloadData.forEach((item, index) => {
       if (errorRowIndex !== 0)
         return
@@ -187,6 +184,15 @@ const TextGeneration: FC<IMainProps> = ({
       promptConfig?.prompt_variables.forEach((varItem, varIndex) => {
         if (errorRowIndex !== 0)
           return
+        if (varItem.type === 'string') {
+          const maxLen = varItem.max_length || DEFAULT_VALUE_MAX_LEN
+          if (item[varIndex].length > maxLen) {
+            moreThanMaxLengthVarName = varItem.name
+            maxLength = maxLen
+            errorRowIndex = index + 1
+            return
+          }
+        }
         if (varItem.required === false)
           return
 
@@ -195,18 +201,15 @@ const TextGeneration: FC<IMainProps> = ({
           errorRowIndex = index + 1
         }
       })
-
-      if (errorRowIndex !== 0)
-        return
-
-      if (item[varLen] === '') {
-        requiredVarName = t('share.generation.queryTitle')
-        errorRowIndex = index + 1
-      }
     })
 
     if (errorRowIndex !== 0) {
-      notify({ type: 'error', message: t('share.generation.errorMsg.invalidLine', { rowIndex: errorRowIndex + 1, varName: requiredVarName }) })
+      if (requiredVarName)
+        notify({ type: 'error', message: t('share.generation.errorMsg.invalidLine', { rowIndex: errorRowIndex + 1, varName: requiredVarName }) })
+
+      if (moreThanMaxLengthVarName)
+        notify({ type: 'error', message: t('share.generation.errorMsg.moreThanMaxLengthLine', { rowIndex: errorRowIndex + 1, varName: moreThanMaxLengthVarName, maxLength }) })
+
       return false
     }
     return true
@@ -234,7 +237,6 @@ const TextGeneration: FC<IMainProps> = ({
         status: i < PARALLEL_LIMIT ? TaskStatus.running : TaskStatus.pending,
         params: {
           inputs,
-          query: item[varLen],
         },
       }
     })
@@ -334,7 +336,6 @@ const TextGeneration: FC<IMainProps> = ({
     promptConfig={promptConfig}
     moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
     inputs={isCallBatchAPI ? (task as Task).params.inputs : inputs}
-    query={isCallBatchAPI ? (task as Task).params.query : query}
     controlSend={controlSend}
     controlStopResponding={controlStopResponding}
     onShowRes={showResSidebar}
@@ -379,7 +380,6 @@ const TextGeneration: FC<IMainProps> = ({
               </div>
             )}
           </div>
-
         </div>
 
         <div className='grow overflow-y-auto'>
@@ -459,8 +459,6 @@ const TextGeneration: FC<IMainProps> = ({
                 inputs={inputs}
                 onInputsChange={setInputs}
                 promptConfig={promptConfig}
-                query={query}
-                onQueryChange={setQuery}
                 onSend={handleSend}
               />
             </div>

+ 0 - 8
web/app/components/share/text-generation/result/index.tsx

@@ -21,7 +21,6 @@ export type IResultProps = {
   promptConfig: PromptConfig | null
   moreLikeThisEnabled: boolean
   inputs: Record<string, any>
-  query: string
   controlSend?: number
   controlStopResponding?: number
   onShowRes: () => void
@@ -39,7 +38,6 @@ const Result: FC<IResultProps> = ({
   promptConfig,
   moreLikeThisEnabled,
   inputs,
-  query,
   controlSend,
   controlStopResponding,
   onShowRes,
@@ -109,14 +107,8 @@ const Result: FC<IResultProps> = ({
     if (!checkCanSend())
       return
 
-    if (!query) {
-      logError(t('appDebug.errorMessage.queryRequired'))
-      return false
-    }
-
     const data = {
       inputs,
-      query,
     }
 
     setMessageId(null)

+ 1 - 1
web/app/components/share/text-generation/run-batch/csv-download/index.tsx

@@ -16,7 +16,7 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
 }) => {
   const { t } = useTranslation()
   const { CSVDownloader, Type } = useCSVDownloader()
-  const addQueryContentVars = [...vars, { name: t('share.generation.queryTitle') }]
+  const addQueryContentVars = [...vars]
   const template = (() => {
     const res: Record<string, string> = {}
     addQueryContentVars.forEach((item) => {

+ 55 - 54
web/app/components/share/text-generation/run-once/index.tsx

@@ -15,20 +15,24 @@ export type IRunOnceProps = {
   promptConfig: PromptConfig
   inputs: Record<string, any>
   onInputsChange: (inputs: Record<string, any>) => void
-  query: string
-  onQueryChange: (query: string) => void
   onSend: () => void
 }
 const RunOnce: FC<IRunOnceProps> = ({
   promptConfig,
   inputs,
   onInputsChange,
-  query,
-  onQueryChange,
   onSend,
 }) => {
   const { t } = useTranslation()
 
+  const onClear = () => {
+    const newInputs: Record<string, any> = {}
+    promptConfig.prompt_variables.forEach((item) => {
+      newInputs[item.key] = ''
+    })
+    onInputsChange(newInputs)
+  }
+
   return (
     <div className="">
       <section>
@@ -38,61 +42,58 @@ const RunOnce: FC<IRunOnceProps> = ({
             <div className='w-full mt-4' key={item.key}>
               <label className='text-gray-900 text-sm font-medium'>{item.name}</label>
               <div className='mt-2'>
-                {item.type === 'select'
-                  ? (
-                    <Select
-                      className='w-full'
-                      defaultValue={inputs[item.key]}
-                      onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
-                      items={(item.options || []).map(i => ({ name: i, value: i }))}
-                      allowSearch={false}
-                      bgClassName='bg-gray-50'
-                    />
-                  )
-                  : (
-                    <input
-                      type="text"
-                      className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
-                      placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                      value={inputs[item.key]}
-                      onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
-                      maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
-                    />
-                  )}
+                {item.type === 'select' && (
+                  <Select
+                    className='w-full'
+                    defaultValue={inputs[item.key]}
+                    onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
+                    items={(item.options || []).map(i => ({ name: i, value: i }))}
+                    allowSearch={false}
+                    bgClassName='bg-gray-50'
+                  />
+                )}
+                {item.type === 'string' && (
+                  <input
+                    type="text"
+                    className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
+                    placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                    value={inputs[item.key]}
+                    onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
+                    maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
+                  />
+                )}
+                {item.type === 'paragraph' && (
+                  <textarea
+                    className="block w-full h-[104px] p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
+                    placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+                    value={inputs[item.key]}
+                    onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
+                  />
+                )}
               </div>
             </div>
           ))}
           {promptConfig.prompt_variables.length > 0 && (
-            <div className='mt-6 h-[1px] bg-gray-100'></div>
+            <div className='mt-4 h-[1px] bg-gray-100'></div>
           )}
-          <div className='w-full mt-5'>
-            <label className='text-gray-900 text-sm font-medium'>{t('share.generation.queryTitle')}</label>
-            <div className="mt-2 overflow-hidden rounded-lg bg-gray-50 ">
-              <div className="px-4 py-2 bg-gray-50 rounded-t-lg">
-                <textarea
-                  value={query}
-                  onChange={(e) => { onQueryChange(e.target.value) }}
-                  rows={4}
-                  className="w-full px-0 text-sm text-gray-900 border-0 bg-gray-50 focus:outline-none placeholder:bg-gray-50"
-                  placeholder={t('share.generation.queryPlaceholder') as string}
-                  required
-                >
-                </textarea>
-              </div>
-              <div className="flex items-center justify-between px-3 py-2">
-                <div className="flex pl-0 space-x-1 sm:pl-2">
-                  <span className="bg-gray-100 text-gray-500 text-xs font-medium mr-2 px-2.5 py-0.5 rounded cursor-pointer">{query?.length}</span>
-                </div>
-                <Button
-                  type="primary"
-                  className='!h-8 !pl-3 !pr-4'
-                  onClick={onSend}
-                  disabled={!query || query === ''}
-                >
-                  <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
-                  <span className='uppercase text-[13px]'>{t('share.generation.run')}</span>
-                </Button>
-              </div>
+          <div className='w-full mt-4'>
+            <div className="flex items-center justify-between">
+              <Button
+                className='!h-8 !p-3'
+                onClick={onClear}
+                disabled={false}
+              >
+                <span className='text-[13px]'>{t('common.operation.clear')}</span>
+              </Button>
+              <Button
+                type="primary"
+                className='!h-8 !pl-3 !pr-4'
+                onClick={onSend}
+                disabled={false}
+              >
+                <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
+                <span className='text-[13px]'>{t('share.generation.run')}</span>
+              </Button>
             </div>
           </div>
         </form>

+ 5 - 0
web/config/index.ts

@@ -91,9 +91,14 @@ export const TONE_LIST = [
   },
 ]
 
+export const getMaxToken = (modelId: string) => {
+  return (modelId === 'gpt-4' || modelId === 'gpt-3.5-turbo-16k') ? 8000 : 4000
+}
+
 export const LOCALE_COOKIE_NAME = 'locale'
 
 export const DEFAULT_VALUE_MAX_LEN = 48
+export const DEFAULT_PARAGRAPH_VALUE_MAX_LEN = 1000
 
 export const zhRegex = /^[\u4E00-\u9FA5]$/m
 export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/m

+ 5 - 1
web/i18n/lang/app-debug.en.ts

@@ -128,11 +128,15 @@ const translation = {
     notStartWithNumber: 'Variable key: {{key}} can not start with a number',
     keyAlreadyExists: 'Variable key: :{{key}} already exists',
   },
+  otherError: {
+    promptNoBeEmpty: 'Prefix prompt can not be empty',
+  },
   variableConig: {
     modalTitle: 'Field settings',
     description: 'Setting for variable {{varName}}',
     fieldType: 'Field type',
-    string: 'Text',
+    string: 'Short Text',
+    paragraph: 'Paragraph',
     select: 'Select',
     notSet: 'Not set, try typing {{input}} in the prefix prompt',
     stringTitle: 'Form text box options',

+ 4 - 0
web/i18n/lang/app-debug.zh.ts

@@ -124,11 +124,15 @@ const translation = {
     notStartWithNumber: '变量: {{key}} 不能以数字开头',
     keyAlreadyExists: '变量:{{key}} 已存在',
   },
+  otherError: {
+    promptNoBeEmpty: '前缀提示词不能为空',
+  },
   variableConig: {
     modalTitle: '变量设置',
     description: '设置变量 {{varName}}',
     fieldType: '字段类型',
     string: '文本',
+    paragraph: '段落',
     select: '下拉选项',
     notSet: '未设置,在 Prompt 中输入 {{input}} 试试',
     stringTitle: '文本框设置',

+ 2 - 1
web/i18n/lang/share-app.en.ts

@@ -58,7 +58,8 @@ const translation = {
       empty: 'Please input content in the uploaded file.',
       fileStructNotMatch: 'The uploaded CSV file not match the struct.',
       emptyLine: 'Row {{rowIndex}} is empty',
-      invalidLine: 'Row {{rowIndex}}: variables value can not be empty',
+      invalidLine: 'Row {{rowIndex}}: {{varName}} value can not be empty',
+      moreThanMaxLengthLine: 'Row {{rowIndex}}: {{varName}} value can not be more than {{maxLength}} characters',
       atLeastOne: 'Please input at least one row in the uploaded file.',
     },
   },

+ 3 - 1
web/i18n/lang/share-app.zh.ts

@@ -31,6 +31,7 @@ const translation = {
       create: '运行一次',
       batch: '批量运行',
       saved: '已保存',
+
     },
     savedNoData: {
       title: '您还没有保存结果!',
@@ -54,7 +55,8 @@ const translation = {
       empty: '上传文件的内容不能为空',
       fileStructNotMatch: '上传文件的内容与结构不匹配',
       emptyLine: '第 {{rowIndex}} 行的内容为空',
-      invalidLine: '第 {{rowIndex}} 行: 变量值必填',
+      invalidLine: '第 {{rowIndex}} 行: {{varName}}值必填',
+      moreThanMaxLengthLine: '第 {{rowIndex}} 行: {{varName}}值超过最大长度 {{maxLength}}',
       atLeastOne: '上传文件的内容不能少于一条',
     },
   },

+ 29 - 18
web/utils/model-config.ts

@@ -1,22 +1,32 @@
-import { UserInputFormItem, } from '@/types/app'
-import { PromptVariable } from '@/models/debug'
+import type { UserInputFormItem } from '@/types/app'
+import type { PromptVariable } from '@/models/debug'
 
 export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | null) => {
-  if (!useInputs) return []
+  if (!useInputs)
+    return []
   const promptVariables: PromptVariable[] = []
   useInputs.forEach((item: any) => {
-    const type = item['text-input'] ? 'string' : 'select'
-    const content = type === 'string' ? item['text-input'] : item['select']
-    if (type === 'string') {
+    const isParagraph = !!item.paragraph
+    const [type, content] = (() => {
+      if (isParagraph)
+        return ['paragraph', item.paragraph]
+
+      if (item['text-input'])
+        return ['string', item['text-input']]
+
+      return ['select', item.select]
+    })()
+    if (type === 'string' || type === 'paragraph') {
       promptVariables.push({
         key: content.variable,
         name: content.label,
         required: content.required,
-        type: 'string',
+        type,
         max_length: content.max_length,
         options: [],
       })
-    } else {
+    }
+    else {
       promptVariables.push({
         key: content.variable,
         name: content.label,
@@ -32,29 +42,30 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
 export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[]) => {
   const userInputs: UserInputFormItem[] = []
   promptVariables.filter(({ key, name }) => {
-    if (key && key.trim() && name && name.trim()) {
+    if (key && key.trim() && name && name.trim())
       return true
-    }
+
     return false
   }).forEach((item: any) => {
-    if (item.type === 'string') {
+    if (item.type === 'string' || item.type === 'paragraph') {
       userInputs.push({
-        'text-input': {
+        [item.type === 'string' ? 'text-input' : 'paragraph']: {
           label: item.name,
           variable: item.key,
-          required: item.required === false ? false : true, // default true
+          required: item.required !== false, // default true
           max_length: item.max_length,
-          default: ''
+          default: '',
         },
       } as any)
-    } else {
+    }
+    else {
       userInputs.push({
-        'select': {
+        select: {
           label: item.name,
           variable: item.key,
-          required: item.required === false ? false : true, // default true
+          required: item.required !== false, // default true
           options: item.options,
-          default: ''
+          default: '',
         },
       } as any)
     }