merge main

This commit is contained in:
Joel
2024-10-28 10:51:02 +08:00
858 changed files with 16206 additions and 17932 deletions

View File

@ -14,9 +14,9 @@ const EmptyElement: FC = () => {
return (
<div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-text-secondary system-md-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-text-tertiary system-sm-regular'>
{t('appAnnotation.noData.description')}
</div>
</div>

View File

@ -2,17 +2,15 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import useSWR from 'swr'
import Input from '@/app/components/base/input'
import { fetchAnnotationsCount } from '@/service/log'
export type QueryParam = {
export interface QueryParam {
keyword?: string
}
type IFilterProps = {
interface IFilterProps {
appId: string
queryParams: QueryParam
setQueryParams: (v: QueryParam) => void
@ -31,22 +29,18 @@ const Filter: FC<IFilterProps> = ({
if (!data)
return null
return (
<div className='flex justify-between flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search') as string}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
<div className='flex justify-between flex-row flex-wrap gap-2 items-center mb-2'>
<Input
wrapperClassName='w-[200px]'
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
/>
{children}
</div>
)

View File

@ -19,7 +19,7 @@ import Switch from '@/app/components/base/switch'
import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation'
import Loading from '@/app/components/base/loading'
import { APP_PAGE_LIMIT } from '@/config'
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import type { AnnotationReplyConfig } from '@/models/debug'
import { sleep } from '@/utils'
import { useProviderContext } from '@/context/provider-context'
@ -27,7 +27,7 @@ import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import type { App } from '@/types/app'
type Props = {
interface Props {
appDetail: App
}
@ -152,8 +152,8 @@ const Annotation: FC<Props> = ({
return (
<div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<div className='grow flex flex-col py-4 '>
<p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
<div className='flex items-center space-x-2'>
{isChatApp && (

View File

@ -4,13 +4,12 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine } from '@remixicon/react'
import { Edit02 } from '../../base/icons/src/vender/line/general'
import s from './style.module.css'
import type { AnnotationItem } from './type'
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
import cn from '@/utils/classnames'
import useTimestamp from '@/hooks/use-timestamp'
type Props = {
interface Props {
list: AnnotationItem[]
onRemove: (id: string) => void
onView: (item: AnnotationItem) => void
@ -27,21 +26,21 @@ const List: FC<Props> = ({
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
return (
<div className='overflow-x-auto'>
<table className={cn(s.logTable, 'w-full min-w-[440px] border-collapse border-0 text-sm')} >
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
<tr className='uppercase'>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
<td className='whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
<thead className='system-xs-medium-uppercase text-text-tertiary'>
<tr>
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
</tr>
</thead>
<tbody className="text-gray-500">
<tbody className="text-text-secondary system-sm-regular">
{list.map(item => (
<tr
key={item.id}
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
className='border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer'
onClick={
() => {
onView(item)
@ -49,16 +48,16 @@ const List: FC<Props> = ({
}
>
<td
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
title={item.question}
>{item.question}</td>
<td
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
title={item.answer}
>{item.answer}</td>
<td>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td>{item.hit_count}</td>
<td className='w-[96px]' onClick={e => e.stopPropagation()}>
<td className='p-3 pr-2'>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='p-3 pr-2'>{item.hit_count}</td>
<td className='w-[96px] p-3 pr-2' onClick={e => e.stopPropagation()}>
{/* Actions */}
<div className='flex space-x-2 text-gray-500'>
<div

View File

@ -1,9 +1,3 @@
.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}

View File

@ -2,18 +2,15 @@
import type { FC, ReactNode } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import ParamsConfig from '@/app/components/app/configuration/config-voice/param-config'
export type IFeaturePanelProps = {
export interface IFeaturePanelProps {
className?: string
headerIcon?: ReactNode
title: ReactNode
headerRight?: ReactNode
hasHeaderBottomBorder?: boolean
isFocus?: boolean
noBodySpacing?: boolean
children?: ReactNode
isShowTextToSpeech?: boolean
}
const FeaturePanel: FC<IFeaturePanelProps> = ({
@ -22,32 +19,20 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
title,
headerRight,
hasHeaderBottomBorder,
isFocus,
noBodySpacing,
children,
isShowTextToSpeech,
}) => {
return (
<div
className={cn(className, isFocus && 'border border-[#2D0DEE]', 'rounded-xl bg-gray-50 pt-2 pb-3', noBodySpacing && '!pb-0')}
style={isFocus
? {
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}
: {}}
>
<div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && '!pb-0', className)}>
{/* Header */}
<div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
<div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}>
<div className='flex justify-between items-center h-8'>
<div className='flex items-center space-x-1 shrink-0'>
{headerIcon && <div className='flex items-center justify-center w-6 h-6'>{headerIcon}</div>}
<div className='text-sm font-semibold text-gray-800'>{title}</div>
<div className='text-text-secondary system-sm-semibold'>{title}</div>
</div>
<div className='flex gap-2 items-center'>
{headerRight && <div>{headerRight}</div>}
{isShowTextToSpeech && <div className='flex items-center'>
<ParamsConfig />
</div>}
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@ import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
type Props = {
interface Props {
showWarning: boolean
onShowEditModal: () => void
}
@ -23,7 +23,7 @@ const HistoryPanel: FC<Props> = ({
return (
<Panel
className='mt-3'
className='mt-2'
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.conversationHistory.title')}</div>

View File

@ -1,19 +1,22 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
interface Props {
className?: string
title: string
children: JSX.Element
}
const Field: FC<Props> = ({
className,
title,
children,
}) => {
return (
<div>
<div className='leading-8 text-[13px] font-medium text-gray-700'>{title}</div>
<div className={cn(className)}>
<div className='text-text-secondary system-sm-semibold leading-8'>{title}</div>
<div>{children}</div>
</div>
)

View File

@ -3,38 +3,43 @@ import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import ModalFoot from '../modal-foot'
import ConfigSelect from '../config-select'
import ConfigString from '../config-string'
import SelectTypeItem from '../select-type-item'
import Field from './field'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { checkKeys, getNewVarInWorkflow } from '@/utils/var'
import ConfigContext from '@/context/debug-configuration'
import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
import type { InputVar, MoreInfo, UploadFileSetting } from '@/app/components/workflow/types'
import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import { ChangeType, InputVarType } from '@/app/components/workflow/types'
import { ChangeType, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
import Checkbox from '@/app/components/base/checkbox'
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
const TEXT_MAX_LENGTH = 256
export type IConfigModalProps = {
export interface IConfigModalProps {
isCreate?: boolean
payload?: InputVar
isShow: boolean
varKeys?: string[]
onClose: () => void
onConfirm: (newValue: InputVar, moreInfo?: MoreInfo) => void
supportFile?: boolean
}
const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
const ConfigModal: FC<IConfigModalProps> = ({
isCreate,
payload,
isShow,
onClose,
onConfirm,
supportFile,
}) => {
const { modelConfig } = useContext(ConfigContext)
const { t } = useTranslation()
@ -48,8 +53,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
}, [isShow])
const isStringInput = type === InputVarType.textInput || type === InputVarType.paragraph
const checkVariableName = useCallback((value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
const checkVariableName = useCallback((value: string, canBeEmpty?: boolean) => {
const { isValid, errorMessageKey } = checkKeys([value], canBeEmpty)
if (!isValid) {
Toast.notify({
type: 'error',
@ -72,9 +77,28 @@ const ConfigModal: FC<IConfigModalProps> = ({
}
}, [])
const handleTypeChange = useCallback((type: InputVarType) => {
return () => {
const newPayload = produce(tempPayload, (draft) => {
draft.type = type
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
if (key !== 'max_length')
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
})
if (type === InputVarType.multiFiles)
draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
}
if (type === InputVarType.paragraph)
draft.max_length = DEFAULT_VALUE_MAX_LEN
})
setTempPayload(newPayload)
}
}, [tempPayload])
const handleVarKeyBlur = useCallback((e: any) => {
const varName = e.target.value
if (!checkVariableName(varName) || tempPayload.label)
if (!checkVariableName(varName, true) || tempPayload.label)
return
setTempPayload((prev) => {
@ -113,7 +137,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
if (isStringInput || type === InputVarType.number) {
onConfirm(tempPayload, moreInfo)
}
else {
else if (type === InputVarType.select) {
if (options?.length === 0) {
Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.atLeastOneOption') })
return
@ -133,6 +157,22 @@ const ConfigModal: FC<IConfigModalProps> = ({
}
onConfirm(tempPayload, moreInfo)
}
else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
if (tempPayload.allowed_file_types?.length === 0) {
const errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('appDebug.variableConfig.file.supportFileTypes') })
Toast.notify({ type: 'error', message: errorMessages })
return
}
if (tempPayload.allowed_file_types?.includes(SupportUploadFileTypes.custom) && !tempPayload.allowed_file_extensions?.length) {
const errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('appDebug.variableConfig.file.custom.name') })
Toast.notify({ type: 'error', message: errorMessages })
return
}
onConfirm(tempPayload, moreInfo)
}
else {
onConfirm(tempPayload, moreInfo)
}
}
return (
@ -145,18 +185,20 @@ const ConfigModal: FC<IConfigModalProps> = ({
<div className='space-y-2'>
<Field title={t('appDebug.variableConfig.fieldType')}>
<div className='flex space-x-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={() => handlePayloadChange('type')(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={() => handlePayloadChange('type')(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={() => handlePayloadChange('type')(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={() => handlePayloadChange('type')(InputVarType.number)} />
<div className='grid grid-cols-3 gap-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={handleTypeChange(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={handleTypeChange(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={handleTypeChange(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={handleTypeChange(InputVarType.number)} />
{supportFile && <>
<SelectTypeItem type={InputVarType.singleFile} selected={type === InputVarType.singleFile} onClick={handleTypeChange(InputVarType.singleFile)} />
<SelectTypeItem type={InputVarType.multiFiles} selected={type === InputVarType.multiFiles} onClick={handleTypeChange(InputVarType.multiFiles)} />
</>}
</div>
</Field>
<Field title={t('appDebug.variableConfig.varName')}>
<input
type='text'
className={inputClassName}
<Input
value={variable}
onChange={e => handlePayloadChange('variable')(e.target.value)}
onBlur={handleVarKeyBlur}
@ -164,9 +206,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
/>
</Field>
<Field title={t('appDebug.variableConfig.labelName')}>
<input
type='text'
className={inputClassName}
<Input
value={label as string}
onChange={e => handlePayloadChange('label')(e.target.value)}
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
@ -185,9 +225,18 @@ const ConfigModal: FC<IConfigModalProps> = ({
</Field>
)}
<Field title={t('appDebug.variableConfig.required')}>
<Switch defaultValue={tempPayload.required} onChange={handlePayloadChange('required')} />
</Field>
{[InputVarType.singleFile, InputVarType.multiFiles].includes(type) && (
<FileUploadSetting
payload={tempPayload as UploadFileSetting}
onChange={(p: UploadFileSetting) => setTempPayload(p as InputVar)}
isMultiple={type === InputVarType.multiFiles}
/>
)}
<div className='!mt-5 flex items-center h-6 space-x-2'>
<Checkbox checked={tempPayload.required} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
<span className='text-text-secondary system-sm-semibold'>{t('appDebug.variableConfig.required')}</span>
</div>
</div>
</div>
<ModalFoot

View File

@ -1,8 +1,9 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import Input from '@/app/components/base/input'
export type IConfigStringProps = {
export interface IConfigStringProps {
value: number | undefined
maxLength: number
modelId: string
@ -21,13 +22,13 @@ const ConfigString: FC<IConfigStringProps> = ({
return (
<div>
<input
<Input
type="number"
max={maxLength}
min={1}
value={value || ''}
onChange={(e) => {
let value = parseInt(e.target.value, 10)
let value = Number.parseInt(e.target.value, 10)
if (value > maxLength)
value = maxLength
@ -36,7 +37,6 @@ const ConfigString: FC<IConfigStringProps> = ({
onChange(value)
}}
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
/>
</div>
)

View File

@ -33,7 +33,7 @@ import { InputVarType } from '@/app/components/workflow/types'
export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
type ExternalDataToolParams = {
interface ExternalDataToolParams {
key: string
type: string
index: number
@ -43,7 +43,7 @@ type ExternalDataToolParams = {
icon_background?: string
}
export type IConfigVarProps = {
export interface IConfigVarProps {
promptVariables: PromptVariable[]
readonly?: boolean
onPromptVariablesChange?: (promptVariables: PromptVariable[]) => void
@ -271,7 +271,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
}
return (
<Panel
className="mt-4"
className="mt-2"
headerIcon={
<VarIcon className='w-4 h-4 text-primary-500' />
}

View File

@ -2,33 +2,39 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import cn from '@/utils/classnames'
import type { InputVarType } from '@/app/components/workflow/types'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
export type ISelectTypeItemProps = {
export interface ISelectTypeItemProps {
type: InputVarType
selected: boolean
onClick: () => void
}
const i18nFileTypeMap: Record<string, string> = {
'file': 'single-file',
'file-list': 'multi-files',
}
const SelectTypeItem: FC<ISelectTypeItemProps> = ({
type,
selected,
onClick,
}) => {
const { t } = useTranslation()
const typeName = t(`appDebug.variableConfig.${type}`)
const typeName = t(`appDebug.variableConfig.${i18nFileTypeMap[type] || type}`)
return (
<div
className={cn(s.item, selected && s.selected, 'space-y-1')}
className={cn(
'flex flex-col justify-center items-center h-[58px] rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg space-y-1',
selected ? 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs system-xs-medium' : ' hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs cursor-pointer system-xs-regular')}
onClick={onClick}
>
<div className='shrink-0'>
<InputVarTypeIcon type={type} className='w-5 h-5' />
</div>
<span className={cn(s.text)}>{typeName}</span>
<span>{typeName}</span>
</div>
)
}

View File

@ -1,40 +0,0 @@
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 58px;
width: 98px;
border-radius: 8px;
border: 1px solid #EAECF0;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
background-color: #fff;
cursor: pointer;
}
.item:not(.selected):hover {
border-color: #B2CCFF;
background-color: #F5F8FF;
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
}
.item.selected {
color: #155EEF;
border-color: #528BFF;
background-color: #F5F8FF;
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
}
.text {
font-size: 13px;
color: #667085;
font-weight: 500;
}
.item.selected .text {
color: #155EEF;
}
.item:not(.selected):hover {
color: #344054;
}

View File

@ -1,61 +1,103 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { useContext } from 'use-context-selector'
import Panel from '../base/feature-panel'
import ParamConfig from './param-config'
import { Vision } from '@/app/components/base/icons/src/vender/features'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import { Eye } from '@/app/components/base/icons/src/vender/solid/general'
// import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import ConfigContext from '@/context/debug-configuration'
// import { Resolution } from '@/types/app'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import Switch from '@/app/components/base/switch'
import type { FileUpload } from '@/app/components/base/features/types'
const ConfigVision: FC = () => {
const { t } = useTranslation()
const {
isShowVisionConfig,
visionConfig,
setVisionConfig,
} = useContext(ConfigContext)
const { isShowVisionConfig } = useContext(ConfigContext)
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const handleChange = useCallback((data: FileUpload) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
enabled: data.enabled,
image: {
enabled: data.enabled,
detail: data.image?.detail,
transfer_methods: data.image?.transfer_methods,
number_limits: data.image?.number_limits,
},
}
})
setFeatures(newFeatures)
}, [featuresStore])
if (!isShowVisionConfig)
return null
return (<>
<Panel
className="mt-4"
headerIcon={
<Eye className='w-4 h-4 text-[#6938EF]'/>
}
title={
<div className='flex items-center'>
<div className='mr-1'>{t('appDebug.vision.name')}</div>
return (
<div className='mt-2 flex items-center gap-2 p-2 rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn'>
<div className='shrink-0 p-1'>
<div className='p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
<Vision className='w-4 h-4 text-text-primary-on-surface' />
</div>
</div>
<div className='grow flex items-center'>
<div className='mr-1 text-text-secondary system-sm-semibold'>{t('appDebug.vision.name')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.description')}
</div>
}
/>
</div>
<div className='shrink-0 flex items-center'>
{/* <div className='mr-2 flex items-center gap-0.5'>
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.description')}
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
}
headerRight={
<div className='flex items-center'>
<ParamConfig />
<div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
<Switch
defaultValue={visionConfig.enabled}
onChange={value => setVisionConfig({
...visionConfig,
enabled: value,
})}
size='md'
</div> */}
{/* <div className='flex items-center gap-1'>
<OptionCard
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={() => handleChange(Resolution.high)}
/>
</div>
}
noBodySpacing
/>
</>
<OptionCard
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange(Resolution.low)}
/>
</div> */}
<ParamConfig />
<div className='ml-1 mr-3 w-[1px] h-3.5 bg-divider-subtle'></div>
<Switch
defaultValue={file?.enabled}
onChange={value => handleChange({
...(file || {}),
enabled: value,
})}
size='md'
/>
</div>
</div>
)
}
export default React.memo(ConfigVision)

View File

@ -1,129 +1,138 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import RadioGroup from './radio-group'
import ConfigContext from '@/context/debug-configuration'
import produce from 'immer'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { Resolution, TransferMethod } from '@/types/app'
import ParamItem from '@/app/components/base/param-item'
import Tooltip from '@/app/components/base/tooltip'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { FileUpload } from '@/app/components/base/features/types'
const MIN = 1
const MAX = 6
const ParamConfigContent: FC = () => {
const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const {
visionConfig,
setVisionConfig,
} = useContext(ConfigContext)
const handleChange = useCallback((data: FileUpload) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const transferMethod = (() => {
if (!visionConfig.transfer_methods || visionConfig.transfer_methods.length === 2)
return TransferMethod.all
return visionConfig.transfer_methods[0]
})()
const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
allowed_file_upload_methods: data.allowed_file_upload_methods,
number_limits: data.number_limits,
image: {
enabled: data.enabled,
detail: data.image?.detail,
transfer_methods: data.allowed_file_upload_methods,
number_limits: data.number_limits,
},
}
})
setFeatures(newFeatures)
}, [featuresStore])
return (
<div>
<div>
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.vision.visionSettings.high'),
value: Resolution.high,
},
{
label: t('appDebug.vision.visionSettings.low'),
value: Resolution.low,
},
]}
value={visionConfig.detail}
onChange={(value: Resolution) => {
setVisionConfig({
...visionConfig,
detail: value,
})
}}
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<div>
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.vision.visionSettings.both'),
value: TransferMethod.all,
},
{
label: t('appDebug.vision.visionSettings.localUpload'),
value: TransferMethod.local_file,
},
{
label: t('appDebug.vision.visionSettings.url'),
value: TransferMethod.remote_url,
},
]}
value={transferMethod}
onChange={(value: TransferMethod) => {
if (value === TransferMethod.all) {
setVisionConfig({
...visionConfig,
transfer_methods: [TransferMethod.remote_url, TransferMethod.local_file],
})
return
}
setVisionConfig({
...visionConfig,
transfer_methods: [value],
})
}}
<div className='flex items-center gap-1'>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={() => handleChange({
...file,
image: { detail: Resolution.high },
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange({
...file,
image: { detail: Resolution.low },
})}
/>
</div>
<div>
<ParamItem
id='upload_limit'
className=''
name={t('appDebug.vision.visionSettings.uploadLimit')}
noTooltip
{...{
default: 2,
step: 1,
min: MIN,
max: MAX,
}}
value={visionConfig.number_limits}
enable={true}
onChange={(_key: string, value: number) => {
if (!value)
return
</div>
<div>
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
<div className='flex items-center gap-1'>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.both')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && !!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.localUpload')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && file?.allowed_file_upload_methods?.length === 1}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.local_file],
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.url')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url) && file?.allowed_file_upload_methods?.length === 1}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.remote_url],
})}
/>
</div>
</div>
<div>
<ParamItem
id='upload_limit'
className=''
name={t('appDebug.vision.visionSettings.uploadLimit')}
noTooltip
{...{
default: 2,
step: 1,
min: MIN,
max: MAX,
}}
value={file?.number_limits || 3}
enable={true}
onChange={(_key: string, value: number) => {
if (!value)
return
setVisionConfig({
...visionConfig,
number_limits: value,
})
}}
/>
</div>
handleChange({
...file,
number_limits: value,
})
}}
/>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import VoiceParamConfig from './param-config-content'
import ParamConfigContent from './param-config-content'
import cn from '@/utils/classnames'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import {
@ -25,14 +25,14 @@ const ParamsConfig: FC = () => {
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-text-tertiary cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
<VoiceParamConfig />
<ParamConfigContent />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -1,40 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'
type OPTION = {
label: string
value: any
}
type Props = {
className?: string
options: OPTION[]
value: any
onChange: (value: any) => void
}
const RadioGroup: FC<Props> = ({
className = '',
options,
value,
onChange,
}) => {
return (
<div className={cn(className, 'flex')}>
{options.map(item => (
<div
key={item.value}
className={cn(s.item, item.value === value && s.checked)}
onClick={() => onChange(item.value)}
>
<div className={s.radio}></div>
<div className='text-[13px] font-medium text-gray-900'>{item.label}</div>
</div>
))}
</div>
)
}
export default React.memo(RadioGroup)

View File

@ -1,24 +0,0 @@
.item {
@apply grow flex items-center h-8 px-2.5 rounded-lg bg-gray-25 border border-gray-100 cursor-pointer space-x-2;
}
.item:hover {
background-color: #ffffff;
border-color: #B2CCFF;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}
.item.checked {
background-color: #ffffff;
border-color: #528BFF;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10);
}
.radio {
@apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
}
.item.checked .radio {
border-width: 5px;
border-color: #155eef;
}

View File

@ -1,220 +0,0 @@
'use client'
import useSWR from 'swr'
import type { FC } from 'react'
import { useContext } from 'use-context-selector'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from '@/utils/classnames'
import RadioGroup from '@/app/components/app/configuration/config-vision/radio-group'
import type { Item } from '@/app/components/base/select'
import ConfigContext from '@/context/debug-configuration'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import { languages } from '@/i18n/language'
import { TtsAutoPlay } from '@/types/app'
const VoiceParamConfig: FC = () => {
const { t } = useTranslation()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const {
textToSpeechConfig,
setTextToSpeechConfig,
} = useContext(ConfigContext)
let languageItem = languages.find(item => item.value === textToSpeechConfig.language)
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
if (languages && !languageItem && languages.length > 0)
languageItem = languages[0]
const language = languageItem?.value
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
let voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)
if (voiceItems && !voiceItem && voiceItems.length > 0)
voiceItem = voiceItems[0]
const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
return (
<div>
<div>
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div
className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.language')}</div>
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<Listbox
value={languageItem}
onChange={(value: Item) => {
setTextToSpeechConfig({
...textToSpeechConfig,
language: String(value.value),
})
}}
>
<div className={'relative h-9'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span className={classNames('block truncate text-left', !languageItem?.name && 'text-gray-400')}>
{languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{languages.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span
className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
{(selected || item.value === textToSpeechConfig.language) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div>
<div
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
<Listbox
value={voiceItem ?? {}}
disabled={!languageItem}
onChange={(value: Item) => {
if (!value.value)
return
setTextToSpeechConfig({
...textToSpeechConfig,
voice: String(value.value),
})
}}
>
<div className={'relative h-9'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span
className={classNames('block truncate text-left', !voiceItem?.name && 'text-gray-400')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{voiceItems?.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{(selected || item.value === textToSpeechConfig.voice) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div>
<div
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.autoPlay')}</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.voice.voiceSettings.autoPlayEnabled'),
value: TtsAutoPlay.enabled,
},
{
label: t('appDebug.voice.voiceSettings.autoPlayDisabled'),
value: TtsAutoPlay.disabled,
},
]}
value={textToSpeechConfig.autoPlay ? textToSpeechConfig.autoPlay : TtsAutoPlay.disabled}
onChange={(value: TtsAutoPlay) => {
setTextToSpeechConfig({
...textToSpeechConfig,
autoPlay: value,
})
}}
/>
</div>
</div>
</div>
</div>
)
}
export default React.memo(VoiceParamConfig)

View File

@ -1,41 +0,0 @@
'use client'
import type { FC } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import VoiceParamConfig from './param-config-content'
import cn from '@/utils/classnames'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
const ParamsConfig: FC = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
<VoiceParamConfig />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ParamsConfig)

View File

@ -58,7 +58,7 @@ const AgentTools: FC = () => {
return (
<>
<Panel
className="mt-4"
className="mt-2"
noBodySpacing={tools.length === 0}
headerIcon={
<RiHammerFill className='w-4 h-4 text-primary-500' />

View File

@ -18,22 +18,23 @@ import cn from 'classnames'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import { generateRule } from '@/service/debug'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import type { Model } from '@/types/app'
import { AppType } from '@/types/app'
import ConfigVar from '@/app/components/app/configuration/config-var'
import OpeningStatement from '@/app/components/app/configuration/features/chat-group/opening-statement'
import GroupName from '@/app/components/app/configuration/base/group-name'
import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
// type
import type { AutomaticRes } from '@/service/debug'
import { Generator } from '@/app/components/base/icons/src/vender/other'
export type IGetAutomaticResProps = {
export interface IGetAutomaticResProps {
mode: AppType
model: Model
isShow: boolean
@ -212,7 +213,11 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
<div className='mt-6'>
<div className='text-[0px]'>
<div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.generate.instruction')}</div>
<textarea className="w-full h-[200px] overflow-y-auto px-3 py-2 text-sm bg-gray-50 rounded-lg" placeholder={t('appDebug.generate.instructionPlaceHolder') as string} value={instruction} onChange={e => setInstruction(e.target.value)} />
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
value={instruction}
onChange={e => setInstruction(e.target.value)} />
</div>
<div className='mt-5 flex justify-end'>
@ -257,10 +262,19 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
{(mode !== AppType.completion && res?.opening_statement) && (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<OpeningStatement
value={res?.opening_statement || ''}
readonly
/>
<div
className='mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'
>
<div className='mb-2 flex items-center gap-2'>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-light-blue-light-500'>
<LoveMessage className='w-4 h-4 text-text-primary-on-surface' />
</div>
<div className='grow flex items-center text-text-secondary system-sm-semibold'>
{t('appDebug.feature.conversationOpener.title')}
</div>
</div>
<div className='min-h-8 text-text-tertiary system-xs-regular'>{res.opening_statement}</div>
</div>
</div>
)}
</>

View File

@ -1,40 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/solid'
export type IAddFeatureBtnProps = {
toBottomHeight: number
onClick: () => void
}
const ITEM_HEIGHT = 48
const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
toBottomHeight,
onClick,
}) => {
const { t } = useTranslation()
return (
<div
className='absolute z-[9] left-0 right-0 flex justify-center pb-4'
style={{
top: toBottomHeight - ITEM_HEIGHT,
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)',
}}
>
<div
className='flex items-center h-8 space-x-2 px-3
border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer
text-xs font-semibold text-primary-600 uppercase
'
onClick={onClick}
>
<PlusIcon className='w-4 h-4 font-semibold' />
<div>{t('appDebug.operation.addFeature')}</div>
</div>
</div>
)
}
export default React.memo(AddFeatureBtn)

View File

@ -1,52 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'
export type IFeatureItemProps = {
icon: React.ReactNode
previewImgClassName?: string
title: string
description: string
value: boolean
onChange: (value: boolean) => void
}
const FeatureItem: FC<IFeatureItemProps> = ({
icon,
previewImgClassName,
title,
description,
value,
onChange,
}) => {
return (
<div className={cn(s.wrap, 'relative flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer')}>
<div className='flex space-x-3 mr-2'>
{/* icon */}
<div
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white'
style={{
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}}
>
{icon}
</div>
<div>
<div className='text-sm font-semibold text-gray-800'>{title}</div>
<div className='text-xs font-normal text-gray-500'>{description}</div>
</div>
</div>
<Switch onChange={onChange} defaultValue={value} />
{
previewImgClassName && (
<div className={cn(s.preview, s[previewImgClassName])}>
</div>)
}
</div>
)
}
export default React.memo(FeatureItem)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 211 KiB

View File

@ -1,41 +0,0 @@
.preview {
display: none;
position: absolute;
top: 0;
left: 100%;
transform: translate(32px, -54px);
width: 280px;
height: 360px;
background: center center no-repeat;
background-size: contain;
border-radius: 8px;
}
.wrap:hover .preview {
display: block;
}
.openingStatementPreview {
background-image: url(./preview-imgs/opening-statement.png);
}
.suggestedQuestionsAfterAnswerPreview {
background-image: url(./preview-imgs/suggested-questions-after-answer.png);
}
.moreLikeThisPreview {
background-image: url(./preview-imgs/more-like-this.png);
}
.speechToTextPreview {
background-image: url(./preview-imgs/speech-to-text.png);
}
.textToSpeechPreview {
@apply shadow-lg rounded-lg;
background-image: url(./preview-imgs/text-to-audio-preview-assistant@2x.png);
}
.citationPreview {
background-image: url(./preview-imgs/citation.png);
}

View File

@ -1,172 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import FeatureGroup from '../feature-group'
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
import FeatureItem from './feature-item'
import Modal from '@/app/components/base/modal'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
import { Microphone01, Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
type IConfig = {
openingStatement: boolean
moreLikeThis: boolean
suggestedQuestionsAfterAnswer: boolean
speechToText: boolean
textToSpeech: boolean
citation: boolean
moderation: boolean
annotation: boolean
}
export type IChooseFeatureProps = {
isShow: boolean
onClose: () => void
config: IConfig
isChatApp: boolean
onChange: (key: string, value: boolean) => void
showTextToSpeechItem?: boolean
showSpeechToTextItem?: boolean
}
const OpeningStatementIcon = (
<svg width="15" height="13" viewBox="0 0 15 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8.33328 0.333252C4.83548 0.333252 1.99995 3.16878 1.99995 6.66659C1.99995 7.37325 2.11594 8.05419 2.33045 8.6906C2.36818 8.80254 2.39039 8.86877 2.40482 8.91762L2.40955 8.93407L2.40705 8.93928C2.38991 8.97462 2.36444 9.02207 2.31681 9.11025L1.21555 11.1486C1.1473 11.2749 1.07608 11.4066 1.02711 11.5212C0.978424 11.6351 0.899569 11.844 0.938369 12.0916C0.98385 12.3819 1.15471 12.6375 1.40556 12.7905C1.61957 12.9211 1.84276 12.9281 1.96659 12.9267C2.09117 12.9252 2.24012 12.9098 2.3829 12.895L5.81954 12.5397C5.87458 12.534 5.90335 12.5311 5.92443 12.5295L5.92715 12.5293L5.93539 12.5322C5.96129 12.5415 5.99642 12.555 6.05705 12.5784C6.76435 12.8509 7.53219 12.9999 8.33328 12.9999C11.8311 12.9999 14.6666 10.1644 14.6666 6.66659C14.6666 3.16878 11.8311 0.333252 8.33328 0.333252ZM5.97966 4.7214C6.73118 4.08722 7.73139 4.27352 8.3312 4.96609C8.931 4.27352 9.9183 4.09389 10.6827 4.7214C11.4472 5.34892 11.5401 6.41591 10.9499 7.16596C10.5843 7.63065 9.66655 8.47935 9.02117 9.05789C8.78411 9.2704 8.66558 9.37666 8.52332 9.41947C8.40129 9.4562 8.2611 9.4562 8.13907 9.41947C7.99682 9.37666 7.87829 9.2704 7.64122 9.05789C6.99584 8.47935 6.07814 7.63065 5.71251 7.16596C5.12234 6.41591 5.22815 5.35559 5.97966 4.7214Z" fill="#DD2590" />
</svg>
)
const ChooseFeature: FC<IChooseFeatureProps> = ({
isShow,
onClose,
isChatApp,
config,
onChange,
showTextToSpeechItem,
showSpeechToTextItem,
}) => {
const { t } = useTranslation()
return (
<Modal
isShow={isShow}
onClose={onClose}
className='w-[400px]'
title={t('appDebug.operation.addFeature')}
closable
overflowVisible
>
<div className='pt-5 pb-10'>
{/* Chat Feature */}
{isChatApp && (
<FeatureGroup
title={t('appDebug.feature.groupChat.title')}
description={t('appDebug.feature.groupChat.description') as string}
>
<>
<FeatureItem
icon={OpeningStatementIcon}
previewImgClassName='openingStatementPreview'
title={t('appDebug.feature.conversationOpener.title')}
description={t('appDebug.feature.conversationOpener.description')}
value={config.openingStatement}
onChange={value => onChange('openingStatement', value)}
/>
<FeatureItem
icon={<SuggestedQuestionsAfterAnswerIcon />}
previewImgClassName='suggestedQuestionsAfterAnswerPreview'
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
value={config.suggestedQuestionsAfterAnswer}
onChange={value => onChange('suggestedQuestionsAfterAnswer', value)}
/>
{
showTextToSpeechItem && (
<FeatureItem
icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='textToSpeechPreview'
title={t('appDebug.feature.textToSpeech.title')}
description={t('appDebug.feature.textToSpeech.description')}
value={config.textToSpeech}
onChange={value => onChange('textToSpeech', value)}
/>
)
}
{
showSpeechToTextItem && (
<FeatureItem
icon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='speechToTextPreview'
title={t('appDebug.feature.speechToText.title')}
description={t('appDebug.feature.speechToText.description')}
value={config.speechToText}
onChange={value => onChange('speechToText', value)}
/>
)
}
<FeatureItem
icon={<Citations className='w-4 h-4 text-[#FD853A]' />}
previewImgClassName='citationPreview'
title={t('appDebug.feature.citation.title')}
description={t('appDebug.feature.citation.description')}
value={config.citation}
onChange={value => onChange('citation', value)}
/>
</>
</FeatureGroup>
)}
{/* Text Generation Feature */}
{!isChatApp && (
<FeatureGroup title={t('appDebug.feature.groupExperience.title')}>
<>
<FeatureItem
icon={<MoreLikeThisIcon />}
previewImgClassName='moreLikeThisPreview'
title={t('appDebug.feature.moreLikeThis.title')}
description={t('appDebug.feature.moreLikeThis.description')}
value={config.moreLikeThis}
onChange={value => onChange('moreLikeThis', value)}
/>
{
showTextToSpeechItem && (
<FeatureItem
icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='textToSpeechPreview'
title={t('appDebug.feature.textToSpeech.title')}
description={t('appDebug.feature.textToSpeech.description')}
value={config.textToSpeech}
onChange={value => onChange('textToSpeech', value)}
/>
)
}
</>
</FeatureGroup>
)}
<FeatureGroup title={t('appDebug.feature.toolbox.title')}>
<>
<FeatureItem
icon={<FileSearch02 className='w-4 h-4 text-[#039855]' />}
previewImgClassName=''
title={t('appDebug.feature.moderation.title')}
description={t('appDebug.feature.moderation.description')}
value={config.moderation}
onChange={value => onChange('moderation', value)}
/>
{isChatApp && (
<FeatureItem
icon={<MessageFast className='w-4 h-4 text-[#444CE7]' />}
title={t('appDebug.feature.annotation.title')}
description={t('appDebug.feature.annotation.description')}
value={config.annotation}
onChange={value => onChange('annotation', value)}
/>
)}
</>
</FeatureGroup>
</div>
</Modal>
)
}
export default React.memo(ChooseFeature)

View File

@ -1,31 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import GroupName from '@/app/components/app/configuration/base/group-name'
export type IFeatureGroupProps = {
title: string
description?: string
children: React.ReactNode
}
const FeatureGroup: FC<IFeatureGroupProps> = ({
title,
description,
children,
}) => {
return (
<div className='mb-6'>
<div className='mb-2'>
<GroupName name={title} />
{description && (
<div className='text-xs font-normal text-gray-500'>{description}</div>
)}
</div>
<div className='space-y-2'>
{children}
</div>
</div>
)
}
export default React.memo(FeatureGroup)

View File

@ -1,70 +1,33 @@
'use client'
import type { FC } from 'react'
import React, { useRef } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { useBoolean, useScroll } from 'ahooks'
import { useFormattingChangedDispatcher } from '../debug/hooks'
import DatasetConfig from '../dataset-config'
import ChatGroup from '../features/chat-group'
import ExperienceEnhanceGroup from '../features/experience-enhance-group'
import Toolbox from '../toolbox'
import HistoryPanel from '../config-prompt/conversation-history/history-panel'
import ConfigVision from '../config-vision'
import useAnnotationConfig from '../toolbox/annotation/use-annotation-config'
import AddFeatureBtn from './feature/add-feature-btn'
import ChooseFeature from './feature/choose-feature'
import useFeature from './feature/use-feature'
import AgentTools from './agent/agent-tools'
import ConfigContext from '@/context/debug-configuration'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var'
import type { CitationConfig, ModelConfig, ModerationConfig, MoreLikeThisConfig, PromptVariable, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig, TextToSpeechConfig } from '@/models/debug'
import type { ModelConfig, PromptVariable } from '@/models/debug'
import type { AppType } from '@/types/app'
import { ModelModeType } from '@/types/app'
import { useModalContext } from '@/context/modal-context'
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
const Config: FC = () => {
const {
appId,
mode,
isAdvancedMode,
modelModeType,
isAgent,
// canReturnToSimpleMode,
// setPromptMode,
hasSetBlockStatus,
showHistoryModal,
introduction,
setIntroduction,
suggestedQuestions,
setSuggestedQuestions,
modelConfig,
setModelConfig,
setPrevPromptConfig,
moreLikeThisConfig,
setMoreLikeThisConfig,
suggestedQuestionsAfterAnswerConfig,
setSuggestedQuestionsAfterAnswerConfig,
speechToTextConfig,
setSpeechToTextConfig,
textToSpeechConfig,
setTextToSpeechConfig,
citationConfig,
setCitationConfig,
annotationConfig,
setAnnotationConfig,
moderationConfig,
setModerationConfig,
} = useContext(ConfigContext)
const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode)
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { setShowModerationSettingModal } = useModalContext()
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const promptTemplate = modelConfig.configs.prompt_template
@ -90,138 +53,11 @@ const Config: FC = () => {
setModelConfig(newModelConfig)
}
const [showChooseFeature, {
setTrue: showChooseFeatureTrue,
setFalse: showChooseFeatureFalse,
}] = useBoolean(false)
const { featureConfig, handleFeatureChange } = useFeature({
introduction,
setIntroduction,
moreLikeThis: moreLikeThisConfig.enabled,
setMoreLikeThis: (value) => {
setMoreLikeThisConfig(produce(moreLikeThisConfig, (draft: MoreLikeThisConfig) => {
draft.enabled = value
}))
},
suggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswerConfig.enabled,
setSuggestedQuestionsAfterAnswer: (value) => {
setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => {
draft.enabled = value
}))
formattingChangedDispatcher()
},
speechToText: speechToTextConfig.enabled,
setSpeechToText: (value) => {
setSpeechToTextConfig(produce(speechToTextConfig, (draft: SpeechToTextConfig) => {
draft.enabled = value
}))
},
textToSpeech: textToSpeechConfig.enabled,
setTextToSpeech: (value) => {
setTextToSpeechConfig(produce(textToSpeechConfig, (draft: TextToSpeechConfig) => {
draft.enabled = value
draft.voice = textToSpeechConfig?.voice
draft.language = textToSpeechConfig?.language
}))
},
citation: citationConfig.enabled,
setCitation: (value) => {
setCitationConfig(produce(citationConfig, (draft: CitationConfig) => {
draft.enabled = value
}))
formattingChangedDispatcher()
},
annotation: annotationConfig.enabled,
setAnnotation: async (value) => {
if (value) {
// eslint-disable-next-line ts/no-use-before-define
setIsShowAnnotationConfigInit(true)
}
else {
// eslint-disable-next-line ts/no-use-before-define
await handleDisableAnnotation(annotationConfig.embedding_model)
}
},
moderation: moderationConfig.enabled,
setModeration: (value) => {
setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
draft.enabled = value
}))
if (value && !moderationConfig.type) {
setShowModerationSettingModal({
payload: {
enabled: true,
type: 'keywords',
config: {
keywords: '',
inputs_config: {
enabled: true,
preset_response: '',
},
},
},
onSaveCallback: setModerationConfig,
onCancelCallback: () => {
setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
draft.enabled = false
showChooseFeatureTrue()
}))
},
})
showChooseFeatureFalse()
}
},
})
const {
handleEnableAnnotation,
setScore,
handleDisableAnnotation,
isShowAnnotationConfigInit,
setIsShowAnnotationConfigInit,
isShowAnnotationFullModal,
setIsShowAnnotationFullModal,
} = useAnnotationConfig({
appId,
annotationConfig,
setAnnotationConfig,
})
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || (featureConfig.textToSpeech && !!text2speechDefaultModel) || featureConfig.citation)
const hasCompletionConfig = !isChatApp && (moreLikeThisConfig.enabled || (featureConfig.textToSpeech && !!text2speechDefaultModel))
const hasToolbox = moderationConfig.enabled || featureConfig.annotation
const wrapRef = useRef<HTMLDivElement>(null)
const wrapScroll = useScroll(wrapRef)
const toBottomHeight = (() => {
if (!wrapRef.current)
return 999
const elem = wrapRef.current
const { clientHeight } = elem
const value = (wrapScroll?.top || 0) + clientHeight
return value
})()
return (
<>
<div
ref={wrapRef}
className="grow h-0 relative px-6 pb-[50px] overflow-y-auto"
>
<AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
{showChooseFeature && (
<ChooseFeature
isShow={showChooseFeature}
onClose={showChooseFeatureFalse}
isChatApp={isChatApp}
config={featureConfig}
onChange={handleFeatureChange}
showSpeechToTextItem={!!speech2textDefaultModel}
showTextToSpeechItem={!!text2speechDefaultModel}
/>
)}
{/* Template */}
<ConfigPrompt
mode={mode as AppType}
@ -253,69 +89,6 @@ const Config: FC = () => {
onShowEditModal={showHistoryModal}
/>
)}
{/* ChatConfig */}
{
hasChatConfig && (
<ChatGroup
isShowOpeningStatement={featureConfig.openingStatement}
openingStatementConfig={
{
value: introduction,
onChange: setIntroduction,
suggestedQuestions,
onSuggestedQuestionsChange: setSuggestedQuestions,
}
}
isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
isShowTextToSpeech={featureConfig.textToSpeech && !!text2speechDefaultModel}
isShowSpeechText={featureConfig.speechToText && !!speech2textDefaultModel}
isShowCitation={featureConfig.citation}
/>
)
}
{/* Text Generation config */}{
hasCompletionConfig && (
<ExperienceEnhanceGroup
isShowMoreLike={moreLikeThisConfig.enabled}
isShowTextToSpeech={featureConfig.textToSpeech && !!text2speechDefaultModel}
/>
)
}
{/* Toolbox */}
{
hasToolbox && (
<Toolbox
showModerationSettings={moderationConfig.enabled}
showAnnotation={isChatApp && featureConfig.annotation}
onEmbeddingChange={handleEnableAnnotation}
onScoreChange={setScore}
/>
)
}
<ConfigParamModal
appId={appId}
isInit
isShow={isShowAnnotationConfigInit}
onHide={() => {
setIsShowAnnotationConfigInit(false)
showChooseFeatureTrue()
}}
onSave={async (embeddingModel, score) => {
await handleEnableAnnotation(embeddingModel, score)
setIsShowAnnotationConfigInit(false)
}}
annotationConfig={annotationConfig}
/>
{isShowAnnotationFullModal && (
<AnnotationFullModal
show={isShowAnnotationFullModal}
onHide={() => setIsShowAnnotationFullModal(false)}
/>
)}
</div>
</>
)

View File

@ -13,6 +13,11 @@ import ContextVar from './context-var'
import ConfigContext from '@/context/debug-configuration'
import { AppType } from '@/types/app'
import type { DataSet } from '@/models/datasets'
import {
getMultipleRetrievalConfig,
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
const Icon = (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -31,13 +36,25 @@ const DatasetConfig: FC = () => {
setModelConfig,
showSelectDataSet,
isAgent,
datasetConfigs,
setDatasetConfigs,
} = useContext(ConfigContext)
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const hasData = dataSet.length > 0
const {
currentModel: currentRerankModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const onRemove = (id: string) => {
setDataSet(dataSet.filter(item => item.id !== id))
const filteredDataSets = dataSet.filter(item => item.id !== id)
setDataSet(filteredDataSets)
const retrievalConfig = getMultipleRetrievalConfig(datasetConfigs as any, filteredDataSets, dataSet, !!currentRerankModel)
setDatasetConfigs({
...(datasetConfigs as any),
...retrievalConfig,
})
formattingChangedDispatcher()
}
@ -70,7 +87,7 @@ const DatasetConfig: FC = () => {
return (
<FeaturePanel
className='mt-3'
className='mt-2'
headerIcon={Icon}
title={t('appDebug.feature.dataSet.title')}
headerRight={

View File

@ -25,7 +25,7 @@ import { useSelectedDatasetsMode } from '@/app/components/workflow/nodes/knowled
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
type Props = {
interface Props {
datasetConfigs: DatasetConfigs
onChange: (configs: DatasetConfigs, isRetrievalModeChange?: boolean) => void
isInWorkflow?: boolean
@ -55,11 +55,12 @@ const ConfigContent: FC<Props> = ({
retrieval_model: RETRIEVE_TYPE.multiWay,
}, isInWorkflow)
}
}, [type])
}, [type, datasetConfigs, isInWorkflow, onChange])
const {
modelList: rerankModelList,
defaultModel: rerankDefaultModel,
currentModel: isRerankDefaultModelValid,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const {
@ -160,14 +161,14 @@ const ConfigContent: FC<Props> = ({
const selectedRerankMode = datasetConfigs.reranking_mode || RerankingModeEnum.RerankingModel
const canManuallyToggleRerank = useMemo(() => {
return !(
(selectedDatasetsMode.allInternal && selectedDatasetsMode.allEconomic)
return (selectedDatasetsMode.allInternal && selectedDatasetsMode.allEconomic)
|| selectedDatasetsMode.allExternal
)
}, [selectedDatasetsMode.allEconomic, selectedDatasetsMode.allExternal, selectedDatasetsMode.allInternal])
const showRerankModel = useMemo(() => {
if (!canManuallyToggleRerank)
return true
else if (canManuallyToggleRerank && !isRerankDefaultModelValid)
return false
return datasetConfigs.reranking_enable
@ -179,7 +180,7 @@ const ConfigContent: FC<Props> = ({
}, [currentRerankModel, showRerankModel, t])
useEffect(() => {
if (!canManuallyToggleRerank && showRerankModel !== datasetConfigs.reranking_enable) {
if (canManuallyToggleRerank && showRerankModel !== datasetConfigs.reranking_enable) {
onChange({
...datasetConfigs,
reranking_enable: showRerankModel,

View File

@ -16,10 +16,9 @@ import type { DataSet } from '@/models/datasets'
import type { DatasetConfigs } from '@/models/debug'
import {
getMultipleRetrievalConfig,
getSelectedDatasetsMode,
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
type ParamsConfigProps = {
interface ParamsConfigProps {
disabled?: boolean
selectedDatasets: DataSet[]
}
@ -37,50 +36,8 @@ const ParamsConfig = ({
const [tempDataSetConfigs, setTempDataSetConfigs] = useState(datasetConfigs)
useEffect(() => {
const {
allEconomic,
allHighQuality,
allHighQualityFullTextSearch,
allHighQualityVectorSearch,
allInternal,
allExternal,
mixtureHighQualityAndEconomic,
inconsistentEmbeddingModel,
mixtureInternalAndExternal,
} = getSelectedDatasetsMode(selectedDatasets)
const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs
let rerankEnable = restConfigs.reranking_enable
if (((allInternal && allEconomic) || allExternal) && !restConfigs.reranking_model?.reranking_provider_name && rerankEnable === undefined)
rerankEnable = false
if (allEconomic || allHighQuality || allHighQualityFullTextSearch || allHighQualityVectorSearch || (allExternal && selectedDatasets.length === 1))
setRerankSettingModalOpen(false)
if (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel || mixtureInternalAndExternal || (allExternal && selectedDatasets.length > 1))
setRerankSettingModalOpen(true)
setTempDataSetConfigs({
...getMultipleRetrievalConfig({
top_k: restConfigs.top_k,
score_threshold: restConfigs.score_threshold,
reranking_model: restConfigs.reranking_model && {
provider: restConfigs.reranking_model.reranking_provider_name,
model: restConfigs.reranking_model.reranking_model_name,
},
reranking_mode: restConfigs.reranking_mode,
weights: restConfigs.weights,
reranking_enable: rerankEnable,
}, selectedDatasets),
reranking_model: restConfigs.reranking_model && {
reranking_provider_name: restConfigs.reranking_model.reranking_provider_name,
reranking_model_name: restConfigs.reranking_model.reranking_model_name,
},
retrieval_model,
score_threshold_enabled,
datasets,
})
}, [selectedDatasets, datasetConfigs])
setTempDataSetConfigs(datasetConfigs)
}, [datasetConfigs])
const {
defaultModel: rerankDefaultModel,
@ -90,7 +47,7 @@ const ParamsConfig = ({
const isValid = () => {
let errMsg = ''
if (tempDataSetConfigs.retrieval_model === RETRIEVE_TYPE.multiWay) {
if (!tempDataSetConfigs.reranking_model?.reranking_model_name && (!rerankDefaultModel && isRerankDefaultModelValid))
if (!tempDataSetConfigs.reranking_model?.reranking_model_name && (rerankDefaultModel && !isRerankDefaultModelValid))
errMsg = t('appDebug.datasetConfig.rerankModelRequired')
}
if (errMsg) {
@ -104,7 +61,6 @@ const ParamsConfig = ({
const handleSave = () => {
if (!isValid())
return
const config = { ...tempDataSetConfigs }
if (config.retrieval_model === RETRIEVE_TYPE.multiWay && !config.reranking_model) {
config.reranking_model = {
@ -129,7 +85,7 @@ const ParamsConfig = ({
reranking_mode: restConfigs.reranking_mode,
weights: restConfigs.weights,
reranking_enable: restConfigs.reranking_enable,
}, selectedDatasets)
}, selectedDatasets, selectedDatasets, !!isRerankDefaultModelValid)
setTempDataSetConfigs({
...retrievalConfig,
@ -174,6 +130,7 @@ const ParamsConfig = ({
<div className='mt-6 flex justify-end'>
<Button className='mr-2 flex-shrink-0' onClick={() => {
setTempDataSetConfigs(datasetConfigs)
setRerankSettingModalOpen(false)
}}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='flex-shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>

View File

@ -10,6 +10,8 @@ import cn from '@/utils/classnames'
import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import type { DataSet } from '@/models/datasets'
import { useToastContext } from '@/app/components/base/toast'
import { updateDatasetSetting } from '@/service/datasets'
@ -31,7 +33,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
type SettingsModalProps = {
interface SettingsModalProps {
currentDataset: DataSet
onCancel: () => void
onSave: (newDataset: DataSet) => void
@ -204,10 +206,10 @@ const SettingsModal: FC<SettingsModalProps> = ({
<div className={labelClass}>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div>
</div>
<input
<Input
value={localeCurrentDataset.name}
onChange={e => handleValueChange('name', e.target.value)}
className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
className='block h-9'
placeholder={t('datasetSettings.form.namePlaceholder') || ''}
/>
</div>
@ -216,10 +218,10 @@ const SettingsModal: FC<SettingsModalProps> = ({
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div>
</div>
<div className='w-full'>
<textarea
<Textarea
value={localeCurrentDataset.description || ''}
onChange={e => handleValueChange('description', e.target.value)}
className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
className='resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
/>
<a className='mt-2 flex items-center h-[18px] px-3 text-xs text-gray-500' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>

View File

@ -16,7 +16,7 @@ import {
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { OnSend } from '@/app/components/base/chat/types'
import type { ChatConfig, OnSend } from '@/app/components/base/chat/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import {
@ -27,8 +27,10 @@ import {
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useFeatures } from '@/app/components/base/features/hooks'
import type { InputForm } from '@/app/components/base/chat/chat/type'
type ChatItemProps = {
interface ChatItemProps {
modelAndParameter: ModelAndParameter
}
const ChatItem: FC<ChatItemProps> = ({
@ -39,11 +41,29 @@ const ChatItem: FC<ChatItemProps> = ({
modelConfig,
appId,
inputs,
visionConfig,
collectionList,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const config = useConfigFromDebugContext()
const features = useFeatures(s => s.features)
const configTemplate = useConfigFromDebugContext()
const config = useMemo(() => {
return {
...configTemplate,
more_like_this: features.moreLikeThis,
opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
sensitive_word_avoidance: features.moderation,
speech_to_text: features.speech2text,
text_to_speech: features.text2speech,
file_upload: features.file,
suggested_questions_after_answer: features.suggested,
retriever_resource: features.citation,
annotation_reply: features.annotationReply,
} as ChatConfig
}, [configTemplate, features])
const inputsForm = useMemo(() => {
return modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
}, [modelConfig.configs.prompt_variables])
const {
chatList,
chatListRef,
@ -55,7 +75,7 @@ const ChatItem: FC<ChatItemProps> = ({
config,
{
inputs,
promptVariables: modelConfig.configs.prompt_variables,
inputsForm,
},
[],
taskId => stopChatMessageResponding(appId, taskId),
@ -84,7 +104,7 @@ const ChatItem: FC<ChatItemProps> = ({
parent_message_id: chatListRef.current.at(-1)?.id || null,
}
if (visionConfig.enabled && files?.length && supportVision)
if ((config.file_upload as any).enabled && files?.length && supportVision)
data.files = files
handleSend(
@ -95,7 +115,7 @@ const ChatItem: FC<ChatItemProps> = ({
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
},
)
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled, chatListRef])
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, chatListRef])
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {

View File

@ -12,24 +12,30 @@ import {
} from './context'
import type { DebugWithMultipleModelContextType } from './context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import ChatInput from '@/app/components/base/chat/chat/chat-input'
import type { VisionFile } from '@/app/components/base/chat/types'
import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useFeatures } from '@/app/components/base/features/hooks'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { InputForm } from '@/app/components/base/chat/chat/type'
const DebugWithMultipleModel = () => {
const {
mode,
speechToTextConfig,
visionConfig,
inputs,
modelConfig,
} = useDebugConfigurationContext()
const speech2text = useFeatures(s => s.features.speech2text)
const file = useFeatures(s => s.features.file)
const {
multipleModelConfigs,
checkCanSend,
} = useDebugWithMultipleModelContext()
const { eventEmitter } = useEventEmitterContextContext()
const isChatMode = mode === 'chat' || mode === 'agent-chat'
const handleSend = useCallback((message: string, files?: VisionFile[]) => {
const handleSend = useCallback((message: string, files?: FileEntity[]) => {
if (checkCanSend && !checkCanSend())
return
@ -92,6 +98,9 @@ const DebugWithMultipleModel = () => {
}
}, [twoLine, threeLine, fourLine])
const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
const inputsForm = modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
return (
<div className='flex flex-col h-full'>
<div
@ -121,18 +130,20 @@ const DebugWithMultipleModel = () => {
))
}
</div>
{
isChatMode && (
<div className='shrink-0 pb-4 px-6'>
<ChatInput
onSend={handleSend}
speechToTextConfig={speechToTextConfig}
visionConfig={visionConfig}
noSpacing
/>
</div>
)
}
{isChatMode && (
<div className='shrink-0 pb-0 px-6'>
<ChatInputArea
showFeatureBar
showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal}
onSend={handleSend}
speechToTextConfig={speech2text as any}
visionConfig={file}
inputs={inputs}
inputsForm={inputsForm}
/>
</div>
)}
</div>
)
}

View File

@ -13,8 +13,9 @@ import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import { TransferMethod } from '@/app/components/base/chat/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import { useFeatures } from '@/app/components/base/features/hooks'
type TextGenerationItemProps = {
interface TextGenerationItemProps {
modelAndParameter: ModelAndParameter
}
const TextGenerationItem: FC<TextGenerationItemProps> = ({
@ -30,16 +31,14 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
introduction,
suggestedQuestionsAfterAnswerConfig,
citationConfig,
moderationConfig,
externalDataToolsConfig,
chatPromptConfig,
completionPromptConfig,
dataSets,
datasetConfigs,
visionConfig,
moreLikeThisConfig,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const features = useFeatures(s => s.features)
const postDatasets = dataSets.map(({ id }) => ({
dataset: {
enabled: true,
@ -54,18 +53,16 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
// features
more_like_this: features.moreLikeThis as any,
sensitive_word_avoidance: features.moderation as any,
text_to_speech: features.text2speech as any,
file_upload: features.file as any,
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
external_data_tools: externalDataToolsConfig,
more_like_this: moreLikeThisConfig,
text_to_speech: {
enabled: false,
voice: '',
language: '',
},
agent_mode: {
enabled: false,
tools: [],
@ -76,9 +73,6 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
datasets: [...postDatasets],
} as any,
},
file_upload: {
image: visionConfig,
},
}
const {
completion,
@ -106,7 +100,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
model_config: configData,
}
if (visionConfig.enabled && files && files?.length > 0) {
if ((config.file_upload as any).enabled && files && files?.length > 0) {
data.files = files.map((item) => {
if (item.transfer_method === TransferMethod.local_file) {
return {
@ -146,6 +140,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
isLoading={!completion && isResponding}
isResponding={isResponding}
isInstalledApp={false}
siteInfo={null}
messageId={messageId}
isError={false}
onRetry={() => { }}

View File

@ -12,7 +12,7 @@ import {
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context'
import {
fetchConversationMessages,
@ -22,12 +22,15 @@ import {
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type'
type DebugWithSingleModelProps = {
interface DebugWithSingleModelProps {
checkCanSend?: () => boolean
}
export type DebugWithSingleModelRefType = {
export interface DebugWithSingleModelRefType {
handleRestart: () => void
}
const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSingleModelProps>(({
@ -38,12 +41,31 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
modelConfig,
appId,
inputs,
visionConfig,
collectionList,
completionParams,
// isShowVisionConfig,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const config = useConfigFromDebugContext()
const features = useFeatures(s => s.features)
const configTemplate = useConfigFromDebugContext()
const config = useMemo(() => {
return {
...configTemplate,
more_like_this: features.moreLikeThis,
opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
sensitive_word_avoidance: features.moderation,
speech_to_text: features.speech2text,
text_to_speech: features.text2speech,
file_upload: features.file,
suggested_questions_after_answer: features.suggested,
retriever_resource: features.citation,
annotation_reply: features.annotationReply,
} as ChatConfig
}, [configTemplate, features])
const inputsForm = useMemo(() => {
return modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
}, [modelConfig.configs.prompt_variables])
const {
chatList,
chatListRef,
@ -60,7 +82,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
config,
{
inputs,
promptVariables: modelConfig.configs.prompt_variables,
inputsForm,
},
[],
taskId => stopChatMessageResponding(appId, taskId),
@ -91,7 +113,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
}
if (visionConfig.enabled && files?.length && supportVision)
if ((config.file_upload as any)?.enabled && files?.length && supportVision)
data.files = files
handleSend(
@ -102,7 +124,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
},
)
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
@ -134,15 +156,22 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
}
}, [handleRestart])
const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
return (
<Chat
config={config}
chatList={chatList}
isResponding={isResponding}
chatContainerClassName='p-6'
chatFooterClassName='px-6 pt-10 pb-4'
chatContainerClassName='px-3 pt-6'
chatFooterClassName='px-3 pt-10 pb-0'
showFeatureBar
showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal}
suggestedQuestions={suggestedQuestions}
onSend={doSend}
inputs={inputs}
inputsForm={inputsForm}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
showPromptLog

View File

@ -1,12 +1,13 @@
'use client'
import type { FC } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { setAutoFreeze } from 'immer'
import produce, { setAutoFreeze } from 'immer'
import { useBoolean } from 'ahooks'
import {
RiAddLine,
RiEqualizer2Line,
RiSparklingFill,
} from '@remixicon/react'
import { useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow'
@ -23,17 +24,20 @@ import {
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
} from './types'
import { AppType, ModelModeType, TransferMethod } from '@/types/app'
import ChatUserInput from '@/app/components/app/configuration/debug/chat-user-input'
import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
import ConfigContext from '@/context/debug-configuration'
import { ToastContext } from '@/app/components/base/toast'
import { sendCompletionMessage } from '@/service/debug'
import Button from '@/app/components/base/button'
import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import TooltipPlus from '@/app/components/base/tooltip'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config'
import type { Inputs } from '@/models/debug'
import { fetchFileUploadConfig } from '@/service/common'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@ -42,8 +46,9 @@ import { useProviderContext } from '@/context/provider-context'
import AgentLogModal from '@/app/components/base/agent-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
type IDebug = {
interface IDebug {
isAPIKeySet: boolean
onSetting: () => void
inputs: Inputs
@ -77,8 +82,6 @@ const Debug: FC<IDebug> = ({
speechToTextConfig,
textToSpeechConfig,
citationConfig,
moderationConfig,
moreLikeThisConfig,
formattingChanged,
setFormattingChanged,
dataSets,
@ -86,12 +89,9 @@ const Debug: FC<IDebug> = ({
completionParams,
hasSetContextVar,
datasetConfigs,
visionConfig,
setVisionConfig,
} = useContext(ConfigContext)
const { eventEmitter } = useEventEmitterContextContext()
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
useEffect(() => {
setAutoFreeze(false)
return () => {
@ -175,7 +175,7 @@ const Debug: FC<IDebug> = ({
}
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
return false
}
return !hasEmptyInput
@ -195,6 +195,8 @@ const Debug: FC<IDebug> = ({
const [completionRes, setCompletionRes] = useState('')
const [messageId, setMessageId] = useState<string | null>(null)
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
const sendTextCompletion = async () => {
if (isResponding) {
@ -225,36 +227,30 @@ const Debug: FC<IDebug> = ({
completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
more_like_this: moreLikeThisConfig,
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
text_to_speech: {
enabled: false,
voice: '',
language: '',
},
agent_mode: {
enabled: false,
tools: [],
},
dataset_configs: {
...datasetConfigs,
datasets: {
datasets: [...postDatasets],
} as any,
},
file_upload: {
image: visionConfig,
agent_mode: {
enabled: false,
tools: [],
},
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
more_like_this: features.moreLikeThis as any,
sensitive_word_avoidance: features.moderation as any,
text_to_speech: features.text2speech as any,
file_upload: features.file as any,
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
}
if (isAdvancedMode) {
@ -267,7 +263,7 @@ const Debug: FC<IDebug> = ({
model_config: postModelConfig,
}
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
if ((features.file as any).enabled && completionFiles && completionFiles?.length > 0) {
data.files = completionFiles.map((item) => {
if (item.transfer_method === TransferMethod.local_file) {
return {
@ -343,7 +339,7 @@ const Debug: FC<IDebug> = ({
)
}
const handleVisionConfigInMultipleModel = () => {
const handleVisionConfigInMultipleModel = useCallback(() => {
if (debugWithMultipleModel && mode) {
const supportedVision = multipleModelConfigs.some((modelConfig) => {
const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider)
@ -351,25 +347,24 @@ const Debug: FC<IDebug> = ({
return currentModel?.features?.includes(ModelFeatureEnum.vision)
})
const {
features,
setFeatures,
} = featuresStore!.getState()
if (supportedVision) {
setVisionConfig({
...visionConfig,
enabled: true,
}, true)
}
else {
setVisionConfig({
...visionConfig,
enabled: false,
}, true)
}
const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
enabled: supportedVision,
}
})
setFeatures(newFeatures)
}
}
}, [debugWithMultipleModel, featuresStore, mode, multipleModelConfigs, textGenerationModelList])
useEffect(() => {
handleVisionConfigInMultipleModel()
}, [multipleModelConfigs, mode])
}, [multipleModelConfigs, mode, handleVisionConfigInMultipleModel])
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
@ -391,49 +386,74 @@ const Debug: FC<IDebug> = ({
adjustModalWidth()
}, [])
const [expanded, setExpanded] = useState(true)
return (
<>
<div className="shrink-0 pt-4 px-6">
<div className='flex items-center justify-between mb-2'>
<div className='h2 '>{t('appDebug.inputs.title')}</div>
<div className="shrink-0">
<div className='flex items-center justify-between px-4 pt-3 pb-2'>
<div className='text-text-primary system-xl-semibold'>{t('appDebug.inputs.title')}</div>
<div className='flex items-center'>
{
debugWithMultipleModel
? (
<>
<Button
variant='secondary-accent'
variant='ghost-accent'
onClick={() => onMultipleModelConfigsChange(true, [...multipleModelConfigs, { id: `${Date.now()}`, model: '', provider: '', parameters: {} }])}
disabled={multipleModelConfigs.length >= 4}
>
<RiAddLine className='mr-1 w-3.5 h-3.5' />
{t('common.modelProvider.addModel')}({multipleModelConfigs.length}/4)
</Button>
<div className='mx-2 w-[1px] h-[14px] bg-gray-200' />
<div className='mx-2 w-[1px] h-[14px] bg-divider-regular' />
</>
)
: null
}
{mode !== AppType.completion && (
<Button variant='secondary-accent' className='gap-1' onClick={clearConversation}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
</Button>
<>
<TooltipPlus
popupContent={t('common.operation.refresh')}
>
<ActionButton onClick={clearConversation}>
<RefreshCcw01 className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
{varList.length > 0 && (
<div className='relative ml-1 mr-2'>
<TooltipPlus
popupContent={t('workflow.panel.userInputField')}
>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
<RiEqualizer2Line className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
{expanded && <div className='absolute z-10 bottom-[-14px] right-[5px] w-3 h-3 bg-components-panel-on-panel-item-bg border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle rotate-45' />}
</div>
)}
</>
)}
</div>
</div>
<PromptValuePanel
appType={mode as AppType}
onSend={handleSendTextCompletion}
inputs={inputs}
visionConfig={{
...visionConfig,
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
}}
onVisionFilesChange={setCompletionFiles}
/>
{mode !== AppType.completion && expanded && (
<div className='mx-3'>
<ChatUserInput inputs={inputs} />
</div>
)}
{mode === AppType.completion && (
<PromptValuePanel
appType={mode as AppType}
onSend={handleSendTextCompletion}
inputs={inputs}
visionConfig={{
...features.file! as VisionSettings,
transfer_methods: features.file!.allowed_file_upload_methods || [],
image_file_size_limit: features.file?.fileUploadConfig?.image_file_size_limit,
}}
onVisionFilesChange={setCompletionFiles}
/>
)}
</div>
{
debugWithMultipleModel && (
@ -481,26 +501,36 @@ const Debug: FC<IDebug> = ({
)}
{/* Text Generation */}
{mode === AppType.completion && (
<div className="mt-6 px-6 pb-4">
<GroupName name={t('appDebug.result')} />
<>
{(completionRes || isResponding) && (
<TextGeneration
className="mt-2"
content={completionRes}
isLoading={!completionRes && isResponding}
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isResponding={isResponding}
isInstalledApp={false}
messageId={messageId}
isError={false}
onRetry={() => { }}
supportAnnotation
appId={appId}
varList={varList}
siteInfo={null}
/>
<>
<div className='mx-4 mt-3'><GroupName name={t('appDebug.result')} /></div>
<div className='mx-3 mb-8'>
<TextGeneration
className="mt-2"
content={completionRes}
isLoading={!completionRes && isResponding}
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isResponding={isResponding}
isInstalledApp={false}
messageId={messageId}
isError={false}
onRetry={() => { }}
supportAnnotation
appId={appId}
varList={varList}
siteInfo={null}
/>
</div>
</>
)}
</div>
{!completionRes && !isResponding && (
<div className='grow flex flex-col items-center justify-center gap-2'>
<RiSparklingFill className='w-12 h-12 text-text-empty-state-icon' />
<div className='text-text-quaternary system-sm-regular'>{t('appDebug.noResult')}</div>
</div>
)}
</>
)}
{mode === AppType.completion && showPromptLogModal && (
<PromptLogModal

View File

@ -1,25 +0,0 @@
'use client'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
const Citation: FC = () => {
const { t } = useTranslation()
return (
<Panel
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.citation.title')}</div>
</div>
}
headerIcon={<Citations className='w-4 h-4 text-[#FD853A]' />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.citation.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(Citation)

View File

@ -1,65 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import GroupName from '../../base/group-name'
import type { IOpeningStatementProps } from './opening-statement'
import OpeningStatement from './opening-statement'
import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
import SpeechToText from './speech-to-text'
import TextToSpeech from './text-to-speech'
import Citation from './citation'
/*
* Include
* 1. Conversation Opener
* 2. Opening Suggestion
* 3. Next question suggestion
*/
type ChatGroupProps = {
isShowOpeningStatement: boolean
openingStatementConfig: IOpeningStatementProps
isShowSuggestedQuestionsAfterAnswer: boolean
isShowSpeechText: boolean
isShowTextToSpeech: boolean
isShowCitation: boolean
}
const ChatGroup: FC<ChatGroupProps> = ({
isShowOpeningStatement,
openingStatementConfig,
isShowSuggestedQuestionsAfterAnswer,
isShowSpeechText,
isShowTextToSpeech,
isShowCitation,
}) => {
const { t } = useTranslation()
return (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<div className='space-y-3'>
{isShowOpeningStatement && (
<OpeningStatement {...openingStatementConfig} />
)}
{isShowSuggestedQuestionsAfterAnswer && (
<SuggestedQuestionsAfterAnswer />
)}
{
isShowTextToSpeech && (
<TextToSpeech />
)
}
{
isShowSpeechText && (
<SpeechToText />
)
}
{
isShowCitation && (
<Citation />
)
}
</div>
</div>
)
}
export default React.memo(ChatGroup)

View File

@ -1,25 +0,0 @@
'use client'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
const SpeechToTextConfig: FC = () => {
const { t } = useTranslation()
return (
<Panel
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.speechToText.title')}</div>
</div>
}
headerIcon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.speechToText.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(SpeechToTextConfig)

View File

@ -1,34 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
import Tooltip from '@/app/components/base/tooltip'
const SuggestedQuestionsAfterAnswer: FC = () => {
const { t } = useTranslation()
return (
<Panel
title={
<div className='flex items-center gap-1'>
<div>{t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}</div>
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
</div>
}
/>
</div>
}
headerIcon={<SuggestedQuestionsAfterAnswerIcon />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(SuggestedQuestionsAfterAnswer)

View File

@ -1,55 +0,0 @@
'use client'
import useSWR from 'swr'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import ConfigContext from '@/context/debug-configuration'
import { languages } from '@/i18n/language'
import { fetchAppVoices } from '@/service/apps'
import AudioBtn from '@/app/components/base/audio-btn'
const TextToSpeech: FC = () => {
const { t } = useTranslation()
const {
textToSpeechConfig,
} = useContext(ConfigContext)
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const language = textToSpeechConfig.language
const languageInfo = languages.find(i => i.value === textToSpeechConfig.language)
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
const voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)
return (
<Panel
title={
<div className='flex items-center'>
<div>{t('appDebug.feature.textToSpeech.title')}</div>
</div>
}
headerIcon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
headerRight={
<div className='text-xs text-gray-500 inline-flex items-center gap-2'>
{languageInfo && (`${languageInfo?.name} - `)}{voiceItem?.name ?? t('appDebug.voice.defaultDisplay')}
{ languageInfo?.example && (
<AudioBtn
value={languageInfo?.example}
isAudition
voice={textToSpeechConfig.voice}
noCache
/>
)}
</div>
}
noBodySpacing
isShowTextToSpeech
/>
)
}
export default React.memo(TextToSpeech)

View File

@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation'
@ -9,17 +10,17 @@ import { useBoolean, useGetState } from 'ahooks'
import { clone, isEqual } from 'lodash-es'
import { CodeBracketIcon } from '@heroicons/react/20/solid'
import { useShallow } from 'zustand/react/shallow'
import Button from '../../base/button'
import Loading from '../../base/loading'
import AppPublisher from '../app-publisher'
import AgentSettingButton from './config/agent-setting-button'
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
import EditHistoryModal from './config-prompt/conversation-history/edit-modal'
import AgentSettingButton from '@/app/components/app/configuration/config/agent-setting-button'
import useAdvancedPromptConfig from '@/app/components/app/configuration/hooks/use-advanced-prompt-config'
import EditHistoryModal from '@/app/components/app/configuration/config-prompt/conversation-history/edit-modal'
import {
useDebugWithSingleOrMultipleModel,
useFormattingChangedDispatcher,
} from './debug/hooks'
import type { ModelAndParameter } from './debug/types'
} from '@/app/components/app/configuration/debug/hooks'
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
import type {
AnnotationReplyConfig,
DatasetConfigs,
@ -38,7 +39,7 @@ import ConfigContext from '@/context/debug-configuration'
import Config from '@/app/components/app/configuration/config'
import Debug from '@/app/components/app/configuration/debug'
import Confirm from '@/app/components/base/confirm'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ToastContext } from '@/app/components/base/toast'
import { fetchAppDetail, updateAppModelConfig } from '@/service/apps'
import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from '@/utils/model-config'
@ -53,7 +54,10 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Drawer from '@/app/components/base/drawer'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import {
useModelListAndDefaultModelAndCurrentProviderAndModel,
useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { fetchCollectionList } from '@/service/tools'
import type { Collection } from '@/app/components/tools/types'
import { useStore as useAppStore } from '@/app/components/app/store'
@ -61,8 +65,14 @@ import {
getMultipleRetrievalConfig,
getSelectedDatasetsMode,
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
import { FeaturesProvider } from '@/app/components/base/features'
import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
import { fetchFileUploadConfig } from '@/service/common'
type PublishConfig = {
interface PublishConfig {
modelConfig: ModelConfig
completionParams: FormValue
}
@ -70,10 +80,15 @@ type PublishConfig = {
const Configuration: FC = () => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { appDetail, setAppSiderbarExpand } = useAppStore(useShallow(state => ({
const { appDetail, showAppConfigureFeaturesModal, setAppSiderbarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
appDetail: state.appDetail,
setAppSiderbarExpand: state.setAppSiderbarExpand,
showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
})))
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
const latestPublishedAt = useMemo(() => appDetail?.model_config.updated_at, [appDetail])
const [formattingChanged, setFormattingChanged] = useState(false)
const { setShowAccountSettingModal } = useModalContext()
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
@ -84,7 +99,6 @@ const Configuration: FC = () => {
const [mode, setMode] = useState('')
const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)
const modalConfig = useMemo(() => appDetail?.model_config || {} as BackendModelConfig, [appDetail])
const [conversationId, setConversationId] = useState<string | null>('')
const media = useBreakpoints()
@ -142,7 +156,6 @@ const Configuration: FC = () => {
const setCompletionParams = (value: FormValue) => {
const params = { ...value }
// eslint-disable-next-line ts/no-use-before-define
if ((!params.stop || params.stop.length === 0) && (modeModeTypeRef.current === ModelModeType.completion)) {
params.stop = getTempStop()
setTempStop([])
@ -158,13 +171,16 @@ const Configuration: FC = () => {
prompt_template: '',
prompt_variables: [] as PromptVariable[],
},
opening_statement: '',
more_like_this: null,
suggested_questions_after_answer: null,
opening_statement: '',
suggested_questions: [],
sensitive_word_avoidance: null,
speech_to_text: null,
text_to_speech: null,
file_upload: null,
suggested_questions_after_answer: null,
retriever_resource: null,
sensitive_word_avoidance: null,
annotation_reply: null,
dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING,
})
@ -207,6 +223,9 @@ const Configuration: FC = () => {
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
const selectedIds = dataSets.map(item => item.id)
const [rerankSettingModalOpen, setRerankSettingModalOpen] = useState(false)
const {
currentModel: currentRerankModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const handleSelect = (data: DataSet[]) => {
if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) {
hideSelectDataSet()
@ -253,7 +272,7 @@ const Configuration: FC = () => {
reranking_mode: restConfigs.reranking_mode,
weights: restConfigs.weights,
reranking_enable: restConfigs.reranking_enable,
}, newDatasets)
}, newDatasets, dataSets, !!currentRerankModel)
setDatasetConfigs({
...retrievalConfig,
@ -274,7 +293,7 @@ const Configuration: FC = () => {
setModelConfig(_publishedConfig.modelConfig)
setCompletionParams(_publishedConfig.completionParams)
setDataSets(modelConfig.dataSets || [])
// feature
// reset feature
setIntroduction(modelConfig.opening_statement!)
setMoreLikeThisConfig(modelConfig.more_like_this || {
enabled: false,
@ -331,7 +350,6 @@ const Configuration: FC = () => {
const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true)
const setPromptMode = async (mode: PromptMode) => {
if (mode === PromptMode.advanced) {
// eslint-disable-next-line ts/no-use-before-define
await migrateToDefaultPrompt()
setCanReturnToSimpleMode(true)
}
@ -422,6 +440,49 @@ const Configuration: FC = () => {
const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
// *** web app features ***
const featuresData: FeaturesData = useMemo(() => {
return {
moreLikeThis: modelConfig.more_like_this || { enabled: false },
opening: {
enabled: !!modelConfig.opening_statement,
opening_statement: modelConfig.opening_statement || '',
suggested_questions: modelConfig.suggested_questions || [],
},
moderation: modelConfig.sensitive_word_avoidance || { enabled: false },
speech2text: modelConfig.speech_to_text || { enabled: false },
text2speech: modelConfig.text_to_speech || { enabled: false },
file: {
image: {
detail: modelConfig.file_upload?.image?.detail || Resolution.high,
enabled: !!modelConfig.file_upload?.image?.enabled,
number_limits: modelConfig.file_upload?.image?.number_limits || 3,
transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled),
allowed_file_types: modelConfig.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3,
fileUploadConfig: fileUploadConfigResponse,
} as FileUpload,
suggested: modelConfig.suggested_questions_after_answer || { enabled: false },
citation: modelConfig.retriever_resource || { enabled: false },
annotationReply: modelConfig.annotation_reply || { enabled: false },
}
}, [fileUploadConfigResponse, modelConfig])
const handleFeaturesChange = useCallback((flag: any) => {
setShowAppConfigureFeaturesModal(true)
if (flag)
formattingChangedDispatcher()
}, [formattingChangedDispatcher, setShowAppConfigureFeaturesModal])
const handleAddPromptVariable = useCallback((variable: PromptVariable[]) => {
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.configs.prompt_variables = variable
})
setModelConfig(newModelConfig)
}, [modelConfig])
useEffect(() => {
(async () => {
const collectionList = await fetchCollectionList()
@ -514,13 +575,16 @@ const Configuration: FC = () => {
modelConfig.dataset_query_variable,
),
},
opening_statement: modelConfig.opening_statement,
more_like_this: modelConfig.more_like_this,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
opening_statement: modelConfig.opening_statement,
suggested_questions: modelConfig.suggested_questions,
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
speech_to_text: modelConfig.speech_to_text,
text_to_speech: modelConfig.text_to_speech,
file_upload: modelConfig.file_upload,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
retriever_resource: modelConfig.retriever_resource,
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
annotation_reply: modelConfig.annotation_reply,
external_data_tools: modelConfig.external_data_tools,
dataSets: datasets || [],
agentConfig: res.mode === 'agent-chat' ? {
@ -547,9 +611,11 @@ const Configuration: FC = () => {
syncToPublishedConfig(config)
setPublishedConfig(config)
const retrievalConfig = getMultipleRetrievalConfig(modelConfig.dataset_configs, datasets, datasets, !!currentRerankModel)
setDatasetConfigs({
retrieval_model: RETRIEVE_TYPE.multiWay,
...modelConfig.dataset_configs,
...retrievalConfig,
})
setHasFetchedDetail(true)
})
@ -588,7 +654,7 @@ const Configuration: FC = () => {
else { return promptEmpty }
})()
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
const onPublish = async (modelAndParameter?: ModelAndParameter) => {
const onPublish = async (modelAndParameter?: ModelAndParameter, features?: FeaturesData) => {
const modelId = modelAndParameter?.model || modelConfig.model_id
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables
@ -620,6 +686,9 @@ const Configuration: FC = () => {
},
}))
const fileUpload = { ...features?.file }
delete fileUpload?.fileUploadConfig
// new model config data struct
const data: BackendModelConfig = {
// Simple Mode prompt
@ -629,14 +698,16 @@ const Configuration: FC = () => {
completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(promptVariables),
dataset_query_variable: contextVar || '',
opening_statement: introduction || '',
suggested_questions: suggestedQuestions || [],
more_like_this: moreLikeThisConfig,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
text_to_speech: textToSpeechConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
// features
more_like_this: features?.moreLikeThis as any,
opening_statement: features?.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features?.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
sensitive_word_avoidance: features?.moderation as any,
speech_to_text: features?.speech2text as any,
text_to_speech: features?.text2speech as any,
file_upload: fileUpload as any,
suggested_questions_after_answer: features?.suggested as any,
retriever_resource: features?.citation as any,
agent_mode: {
...modelConfig.agentConfig,
strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
@ -653,9 +724,6 @@ const Configuration: FC = () => {
datasets: [...postDatasets],
} as any,
},
file_upload: {
image: visionConfig,
},
}
if (isAdvancedMode) {
@ -683,12 +751,6 @@ const Configuration: FC = () => {
return true
}
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
const resetAppConfig = () => {
syncToPublishedConfig(publishedConfig!)
setRestoreConfirmOpen(false)
}
const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
const {
@ -787,153 +849,160 @@ const Configuration: FC = () => {
setRerankSettingModalOpen,
}}
>
<>
<div className="flex flex-col h-full">
<div className='relative flex grow h-[200px] pt-14'>
{/* Header */}
<div className='absolute top-0 left-0 w-full bg-white h-14'>
<div className='flex items-center justify-between px-6 h-14'>
<div className='flex items-center'>
<div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
<div className='flex items-center h-[14px] space-x-1 text-xs'>
{isAdvancedMode && (
<div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
<FeaturesProvider features={featuresData}>
<>
<div className="flex flex-col h-full">
<div className='relative flex grow h-[200px] pt-14'>
{/* Header */}
<div className='absolute top-0 left-0 w-full bg-white h-14'>
<div className='flex items-center justify-between px-6 h-14'>
<div className='flex items-center'>
<div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
<div className='flex items-center h-[14px] space-x-1 text-xs'>
{isAdvancedMode && (
<div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
)}
</div>
</div>
<div className='flex items-center'>
{/* Agent Setting */}
{isAgent && (
<AgentSettingButton
isChatModel={modelConfig.mode === ModelModeType.chat}
agentConfig={modelConfig.agentConfig}
isFunctionCall={isFunctionCall}
onAgentSettingChange={(config) => {
const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig = config
})
setModelConfig(nextConfig)
}}
/>
)}
{/* Model and Parameters */}
{!debugWithMultipleModel && (
<>
<ModelParameterModal
isAdvancedMode={isAdvancedMode}
mode={mode}
provider={modelConfig.provider}
completionParams={completionParams}
modelId={modelConfig.model_id}
setModel={setModel as any}
onCompletionParamsChange={(newParams: FormValue) => {
setCompletionParams(newParams)
}}
debugWithMultipleModel={debugWithMultipleModel}
onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
/>
<div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
</>
)}
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="w-4 h-4 text-gray-500" />
</Button>
)}
<AppPublisher {...{
publishDisabled: cannotPublish,
publishedAt: (latestPublishedAt || 0) * 1000,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
publishedConfig: publishedConfig!,
resetAppConfig: () => syncToPublishedConfig(publishedConfig!),
}} />
</div>
</div>
<div className='flex items-center'>
{/* Agent Setting */}
{isAgent && (
<AgentSettingButton
isChatModel={modelConfig.mode === ModelModeType.chat}
agentConfig={modelConfig.agentConfig}
isFunctionCall={isFunctionCall}
onAgentSettingChange={(config) => {
const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig = config
})
setModelConfig(nextConfig)
}}
/>
)}
{/* Model and Parameters */}
{!debugWithMultipleModel && (
<>
<ModelParameterModal
isAdvancedMode={isAdvancedMode}
mode={mode}
provider={modelConfig.provider}
completionParams={completionParams}
modelId={modelConfig.model_id}
setModel={setModel as any}
onCompletionParamsChange={(newParams: FormValue) => {
setCompletionParams(newParams)
}}
debugWithMultipleModel={debugWithMultipleModel}
onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
/>
<div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
</>
)}
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="w-4 h-4 text-gray-500" />
</Button>
)}
<AppPublisher {...{
publishDisabled: cannotPublish,
publishedAt: (modalConfig.created_at || 0) * 1000,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
onRestore: () => setRestoreConfirmOpen(true),
}} />
</div>
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
<Config />
</div>
{!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='grow flex flex-col border-t-[0.5px] border-l-[0.5px] rounded-tl-2xl border-components-panel-border bg-chatbot-bg '>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
/>
</div>
</div>
</div>}
</div>
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
<Config />
</div>
{!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
/>
</div>
</div>}
</div>
</div>
{restoreConfirmOpen && (
<Confirm
title={t('appDebug.resetConfig.title')}
content={t('appDebug.resetConfig.message')}
isShow={restoreConfirmOpen}
onConfirm={resetAppConfig}
onCancel={() => setRestoreConfirmOpen(false)}
/>
)}
{showUseGPT4Confirm && (
<Confirm
title={t('appDebug.trailUseGPT4Info.title')}
content={t('appDebug.trailUseGPT4Info.description')}
isShow={showUseGPT4Confirm}
onConfirm={() => {
setShowAccountSettingModal({ payload: 'provider' })
setShowUseGPT4Confirm(false)
}}
onCancel={() => setShowUseGPT4Confirm(false)}
/>
)}
{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={hideSelectDataSet}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}
{isShowHistoryModal && (
<EditHistoryModal
isShow={isShowHistoryModal}
saveLoading={false}
onClose={hideHistoryModal}
data={completionPromptConfig.conversation_histories_role}
onSave={(data) => {
setConversationHistoriesRole(data)
hideHistoryModal()
}}
/>
)}
{isMobile && (
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
{showUseGPT4Confirm && (
<Confirm
title={t('appDebug.trailUseGPT4Info.title')}
content={t('appDebug.trailUseGPT4Info.description')}
isShow={showUseGPT4Confirm}
onConfirm={() => {
setShowAccountSettingModal({ payload: 'provider' })
setShowUseGPT4Confirm(false)
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
onCancel={() => setShowUseGPT4Confirm(false)}
/>
</Drawer>
)}
</>
)}
{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={hideSelectDataSet}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}
{isShowHistoryModal && (
<EditHistoryModal
isShow={isShowHistoryModal}
saveLoading={false}
onClose={hideHistoryModal}
data={completionPromptConfig.conversation_histories_role}
onSave={(data) => {
setConversationHistoriesRole(data)
hideHistoryModal()
}}
/>
)}
{isMobile && (
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
/>
</Drawer>
)}
{showAppConfigureFeaturesModal && (
<NewFeaturePanel
show
inWorkflow={false}
showFileUpload={false}
isChatMode={mode !== 'completion'}
disabled={false}
onChange={handleFeaturesChange}
onClose={() => setShowAppConfigureFeaturesModal(false)}
promptVariables={modelConfig.configs.prompt_variables}
onAutoAddPromptVariable={handleAddPromptVariable}
/>
)}
</>
</FeaturesProvider>
</ConfigContext.Provider>
)
}

View File

@ -1,26 +1,29 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiArrowDownSLine,
RiArrowRightLine,
RiArrowRightSLine,
RiPlayLargeFill,
} from '@remixicon/react'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
import ConfigContext from '@/context/debug-configuration'
import type { Inputs, PromptVariable } from '@/models/debug'
import type { Inputs } from '@/models/debug'
import { AppType, ModelModeType } from '@/types/app'
import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import Tooltip from '@/app/components/base/tooltip'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import type { VisionFile, VisionSettings } from '@/types/app'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { useStore as useAppStore } from '@/app/components/app/store'
import cn from '@/utils/classnames'
export type IPromptValuePanelProps = {
export interface IPromptValuePanelProps {
appType: AppType
onSend?: () => void
inputs: Inputs
@ -42,15 +45,15 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
return key && key?.trim() && name && name?.trim()
})
const promptVariableObj = (() => {
const promptVariableObj = useMemo(() => {
const obj: Record<string, boolean> = {}
promptVariables.forEach((input) => {
obj[input.key] = true
})
return obj
})()
}, [promptVariables])
const canNotRun = (() => {
const canNotRun = useMemo(() => {
if (mode !== AppType.completion)
return true
@ -61,19 +64,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
}
else { return !modelConfig.configs.prompt_template }
})()
const renderRunButton = () => {
return (
<Button
variant="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>
)
}
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
const handleInputValueChange = (key: string, value: string) => {
if (!(key in promptVariableObj))
return
@ -94,159 +86,129 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
setInputs(newInputs)
}
const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
return (
<div className="pb-3 border border-gray-200 bg-white rounded-xl" style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}}>
<div className={'mt-3 px-4 bg-white'}>
<div className={
`${!userInputFieldCollapse && 'mb-2'}`
}>
<div className='flex items-center space-x-1 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
{
userInputFieldCollapse
? <RiArrowRightLine className='w-3 h-3 text-gray-300' />
: <RiArrowDownSLine className='w-3 h-3 text-gray-300' />
}
<div className='text-xs font-medium text-gray-800 uppercase'>{t('appDebug.inputs.userInputField')}</div>
<>
<div className='relative z-[1] mx-3 border-[0.5px] bg-components-panel-on-panel-item-bg border-components-panel-border-subtle rounded-xl shadow-md'>
<div className={cn('px-4 pt-3', userInputFieldCollapse ? 'pb-3' : 'pb-1')}>
<div className='flex items-center gap-0.5 py-0.5 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
<div className='text-text-secondary system-md-semibold-uppercase'>{t('appDebug.inputs.userInputField')}</div>
{userInputFieldCollapse && <RiArrowRightSLine className='w-4 h-4 text-text-secondary'/>}
{!userInputFieldCollapse && <RiArrowDownSLine className='w-4 h-4 text-text-secondary'/>}
</div>
{appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
<div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
{!userInputFieldCollapse && (
<div className='mt-1 text-text-tertiary system-xs-regular'>{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="xl:flex justify-between">
<div className="mr-1 py-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) }}
/>
)}
{type === 'number' && (
<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="number"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
</div>
))}
{!userInputFieldCollapse && promptVariables.length > 0 && (
<div className='px-4 pt-3 pb-4'>
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
<div
key={key}
className='mb-4 last-of-type:mb-0'
>
<div>
<div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
<div className='truncate'>{name || key}</div>
{!required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
</div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
)
}
{
appType === AppType.completion && visionConfig?.enabled && (
<div className="mt-3 xl:flex justify-between">
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
<div className='grow'>
<TextGenerationImageUploader
settings={visionConfig}
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
type: 'image',
transfer_method: fileItem.type,
url: fileItem.url,
upload_file_id: fileItem.fileId,
})))}
/>
{type === 'string' && (
<Input
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{type === 'paragraph' && (
<Textarea
className='grow h-[120px]'
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
{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 === 'number' && (
<Input
type='number'
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div>
</div>
)
}
</>
)
}
</div>
{
appType === AppType.completion && (
<div>
<div className="mt-5 border-b border-gray-100"></div>
<div className="flex justify-between mt-4 px-4">
<Button
onClick={onClear}
disabled={false}
>
<span className='text-[13px]'>{t('common.operation.clear')}</span>
</Button>
{canNotRun
? (<Tooltip
popupContent={t('appDebug.otherError.promptNoBeEmpty')}
needsDelay
>
{renderRunButton()}
</Tooltip>)
: renderRunButton()}
</div>
</div>
))}
{visionConfig?.enabled && (
<div className="mt-3 xl:flex justify-between">
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
<div className='grow'>
<TextGenerationImageUploader
settings={visionConfig}
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
type: 'image',
transfer_method: fileItem.type,
url: fileItem.url,
upload_file_id: fileItem.fileId,
})))}
/>
</div>
</div>
)}
</div>
)
}
</div>
)}
{!userInputFieldCollapse && (
<div className='flex justify-between p-4 pt-3 border-t border-divider-subtle'>
<Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button>
{canNotRun && (
<Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')} needsDelay>
<Button
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
className="w-[96px]">
<RiPlayLargeFill className="shrink-0 w-4 h-4 mr-0.5" aria-hidden="true" />
{t('appDebug.inputs.run')}
</Button>
</Tooltip>
)}
{!canNotRun && (
<Button
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
className="w-[96px]">
<RiPlayLargeFill className="shrink-0 w-4 h-4 mr-0.5" aria-hidden="true" />
{t('appDebug.inputs.run')}
</Button>
)}
</div>
)}
</div>
<div className='mx-3'>
<FeatureBar
showFileUpload={false}
isChatMode={appType !== AppType.completion}
onFeatureBarClick={setShowAppConfigureFeaturesModal} />
</div>
</>
)
}
export default React.memo(PromptValuePanel)
function replaceStringWithValuesWithFormat(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return `<div class='inline-block px-1 rounded-md text-gray-900' style='background: rgba(16, 24, 40, 0.1)'>${name}</div>`
}
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return `<div class='inline-block px-1 rounded-md text-gray-500' style='background: rgba(16, 24, 40, 0.05)'>${valueObj ? valueObj.name : match}</div>`
})
}
export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return name
}
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return valueObj ? `{{${valueObj.name}}}` : match
})
}
// \n -> br
function format(str: string) {
return str.replaceAll('\n', '<br>')
}

View File

@ -1,135 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useRef, useState } from 'react'
import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { MessageCheckRemove, MessageFastPlus } from '@/app/components/base/icons/src/vender/line/communication'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import { Edit04 } from '@/app/components/base/icons/src/vender/line/general'
import RemoveAnnotationConfirmModal from '@/app/components/app/annotation/remove-annotation-confirm-modal'
import Tooltip from '@/app/components/base/tooltip'
import { addAnnotation, delAnnotation } from '@/service/annotation'
import Toast from '@/app/components/base/toast'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
type Props = {
appId: string
messageId?: string
annotationId?: string
className?: string
cached: boolean
query: string
answer: string
onAdded: (annotationId: string, authorName: string) => void
onEdit: () => void
onRemoved: () => void
}
const CacheCtrlBtn: FC<Props> = ({
className,
cached,
query,
answer,
appId,
messageId,
annotationId,
onAdded,
onEdit,
onRemoved,
}) => {
const { t } = useTranslation()
const { plan, enableBilling } = useProviderContext()
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
const { setShowAnnotationFullModal } = useModalContext()
const [showModal, setShowModal] = useState(false)
const cachedBtnRef = useRef<HTMLDivElement>(null)
const isCachedBtnHovering = useHover(cachedBtnRef)
const handleAdd = async () => {
if (isAnnotationFull) {
setShowAnnotationFullModal()
return
}
const res: any = await addAnnotation(appId, {
message_id: messageId,
question: query,
answer,
})
Toast.notify({
message: t('common.api.actionSuccess') as string,
type: 'success',
})
onAdded(res.id, res.account?.name)
}
const handleRemove = async () => {
await delAnnotation(appId, annotationId!)
Toast.notify({
message: t('common.api.actionSuccess') as string,
type: 'success',
})
onRemoved()
setShowModal(false)
}
return (
<div className={cn('inline-block', className)}>
<div className='inline-flex p-0.5 space-x-0.5 rounded-lg bg-white border border-gray-100 shadow-md text-gray-500 cursor-pointer'>
{cached
? (
<div>
<div
ref={cachedBtnRef}
className={cn(isCachedBtnHovering ? 'bg-[#FEF3F2] text-[#D92D20]' : 'bg-[#EEF4FF] text-[#444CE7]', 'flex p-1 space-x-1 items-center rounded-md leading-4 text-xs font-medium')}
onClick={() => setShowModal(true)}
>
{!isCachedBtnHovering
? (
<>
<MessageFast className='w-4 h-4' />
<div>{t('appDebug.feature.annotation.cached')}</div>
</>
)
: <>
<MessageCheckRemove className='w-4 h-4' />
<div>{t('appDebug.feature.annotation.remove')}</div>
</>}
</div>
</div>
)
: answer
? (
<Tooltip
popupContent={t('appDebug.feature.annotation.add')}
>
<div
className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
onClick={handleAdd}
>
<MessageFastPlus className='w-4 h-4' />
</div>
</Tooltip>
)
: null
}
<Tooltip
popupContent={t('appDebug.feature.annotation.edit')}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
onClick={onEdit}
>
<Edit04 className='w-4 h-4' />
</div>
</Tooltip>
</div>
<RemoveAnnotationConfirmModal
isShow={showModal}
onHide={() => setShowModal(false)}
onRemove={handleRemove}
/>
</div>
)
}
export default React.memo(CacheCtrlBtn)

View File

@ -1,139 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ScoreSlider from '../score-slider'
import { Item } from './config-param'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import type { AnnotationReplyConfig } from '@/models/debug'
import { ANNOTATION_DEFAULT } from '@/config'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type Props = {
appId: string
isShow: boolean
onHide: () => void
onSave: (embeddingModel: {
embedding_provider_name: string
embedding_model_name: string
}, score: number) => void
isInit?: boolean
annotationConfig: AnnotationReplyConfig
}
const ConfigParamModal: FC<Props> = ({
isShow,
onHide: doHide,
onSave,
isInit,
annotationConfig: oldAnnotationConfig,
}) => {
const { t } = useTranslation()
const {
modelList: embeddingsModelList,
defaultModel: embeddingsDefaultModel,
currentModel: isEmbeddingsDefaultModelValid,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textEmbedding)
const [annotationConfig, setAnnotationConfig] = useState(oldAnnotationConfig)
const [isLoading, setLoading] = useState(false)
const [embeddingModel, setEmbeddingModel] = useState(oldAnnotationConfig.embedding_model
? {
providerName: oldAnnotationConfig.embedding_model.embedding_provider_name,
modelName: oldAnnotationConfig.embedding_model.embedding_model_name,
}
: (embeddingsDefaultModel
? {
providerName: embeddingsDefaultModel.provider.provider,
modelName: embeddingsDefaultModel.model,
}
: undefined))
const onHide = () => {
if (!isLoading)
doHide()
}
const handleSave = async () => {
if (!embeddingModel || !embeddingModel.modelName || (embeddingModel.modelName === embeddingsDefaultModel?.model && !isEmbeddingsDefaultModelValid)) {
Toast.notify({
message: t('common.modelProvider.embeddingModel.required'),
type: 'error',
})
return
}
setLoading(true)
await onSave({
embedding_provider_name: embeddingModel.providerName,
embedding_model_name: embeddingModel.modelName,
}, annotationConfig.score_threshold)
setLoading(false)
}
return (
<Modal
isShow={isShow}
onClose={onHide}
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
>
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
{t(`appAnnotation.initSetup.${isInit ? 'title' : 'configTitle'}`)}
</div>
<div className='mt-6 space-y-3'>
<Item
title={t('appDebug.feature.annotation.scoreThreshold.title')}
tooltip={t('appDebug.feature.annotation.scoreThreshold.description')}
>
<ScoreSlider
className='mt-1'
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
onChange={(val) => {
setAnnotationConfig({
...annotationConfig,
score_threshold: val / 100,
})
}}
/>
</Item>
<Item
title={t('common.modelProvider.embeddingModel.key')}
tooltip={t('appAnnotation.embeddingModelSwitchTip')}
>
<div className='pt-1'>
<ModelSelector
defaultModel={embeddingModel && {
provider: embeddingModel.providerName,
model: embeddingModel.modelName,
}}
modelList={embeddingsModelList}
onSelect={(val) => {
setEmbeddingModel({
providerName: val.provider,
modelName: val.model,
})
}}
/>
</div>
</Item>
</div>
<div className='mt-6 flex gap-2 justify-end'>
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button
variant='primary'
onClick={handleSave}
loading={isLoading}
>
<div></div>
<div>{t(`appAnnotation.initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`)}</div>
</Button >
</div >
</Modal >
)
}
export default React.memo(ConfigParamModal)

View File

@ -1,4 +0,0 @@
export enum PageType {
log = 'log',
annotation = 'annotation',
}

View File

@ -1,89 +0,0 @@
import React, { useState } from 'react'
import produce from 'immer'
import type { AnnotationReplyConfig } from '@/models/debug'
import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation'
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
import { AnnotationEnableStatus, JobStatus } from '@/app/components/app/annotation/type'
import { sleep } from '@/utils'
import { ANNOTATION_DEFAULT } from '@/config'
import { useProviderContext } from '@/context/provider-context'
type Params = {
appId: string
annotationConfig: AnnotationReplyConfig
setAnnotationConfig: (annotationConfig: AnnotationReplyConfig) => void
}
const useAnnotationConfig = ({
appId,
annotationConfig,
setAnnotationConfig,
}: Params) => {
const { plan, enableBilling } = useProviderContext()
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
const [isShowAnnotationFullModal, setIsShowAnnotationFullModal] = useState(false)
const [isShowAnnotationConfigInit, doSetIsShowAnnotationConfigInit] = React.useState(false)
const setIsShowAnnotationConfigInit = (isShow: boolean) => {
if (isShow) {
if (isAnnotationFull) {
setIsShowAnnotationFullModal(true)
return
}
}
doSetIsShowAnnotationConfigInit(isShow)
}
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
let isCompleted = false
while (!isCompleted) {
const res: any = await queryAnnotationJobStatus(appId, status, jobId)
isCompleted = res.job_status === JobStatus.completed
if (isCompleted)
break
await sleep(2000)
}
}
const handleEnableAnnotation = async (embeddingModel: EmbeddingModelConfig, score?: number) => {
if (isAnnotationFull)
return
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.enable, embeddingModel, score)
await ensureJobCompleted(jobId, AnnotationEnableStatus.enable)
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
draft.enabled = true
draft.embedding_model = embeddingModel
if (!draft.score_threshold)
draft.score_threshold = ANNOTATION_DEFAULT.score_threshold
}))
}
const setScore = (score: number, embeddingModel?: EmbeddingModelConfig) => {
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
draft.score_threshold = score
if (embeddingModel)
draft.embedding_model = embeddingModel
}))
}
const handleDisableAnnotation = async (embeddingModel: EmbeddingModelConfig) => {
if (!annotationConfig.enabled)
return
await updateAnnotationStatus(appId, AnnotationEnableStatus.disable, embeddingModel)
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
draft.enabled = false
}))
}
return {
handleEnableAnnotation,
handleDisableAnnotation,
isShowAnnotationConfigInit,
setIsShowAnnotationConfigInit,
isShowAnnotationFullModal,
setIsShowAnnotationFullModal,
setScore,
}
}
export default useAnnotationConfig

View File

@ -1,79 +0,0 @@
import type { FC } from 'react'
import { useContext } from 'use-context-selector'
import type { CodeBasedExtensionForm } from '@/models/common'
import I18n from '@/context/i18n'
import { PortalSelect } from '@/app/components/base/select'
import type { ModerationConfig } from '@/models/debug'
type FormGenerationProps = {
forms: CodeBasedExtensionForm[]
value: ModerationConfig['config']
onChange: (v: Record<string, string>) => void
}
const FormGeneration: FC<FormGenerationProps> = ({
forms,
value,
onChange,
}) => {
const { locale } = useContext(I18n)
const handleFormChange = (type: string, v: string) => {
onChange({ ...value, [type]: v })
}
return (
<>
{
forms.map((form, index) => (
<div
key={index}
className='py-2'
>
<div className='flex items-center h-9 text-sm font-medium text-gray-900'>
{locale === 'zh-Hans' ? form.label['zh-Hans'] : form.label['en-US']}
</div>
{
form.type === 'text-input' && (
<input
value={value?.[form.variable] || ''}
className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
placeholder={form.placeholder}
onChange={e => handleFormChange(form.variable, e.target.value)}
/>
)
}
{
form.type === 'paragraph' && (
<div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
<textarea
value={value?.[form.variable] || ''}
className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
placeholder={form.placeholder}
onChange={e => handleFormChange(form.variable, e.target.value)}
/>
</div>
)
}
{
form.type === 'select' && (
<PortalSelect
value={value?.[form.variable]}
items={form.options.map((option) => {
return {
name: option.label[locale === 'zh-Hans' ? 'zh-Hans' : 'en-US'],
value: option.value,
}
})}
onSelect={item => handleFormChange(form.variable, item.value as string)}
popupClassName='w-[576px] !z-[102]'
/>
)
}
</div>
))
}
</>
)
}
export default FormGeneration

View File

@ -1,80 +0,0 @@
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import ConfigContext from '@/context/debug-configuration'
import { fetchCodeBasedExtensionList } from '@/service/common'
import I18n from '@/context/i18n'
const Moderation = () => {
const { t } = useTranslation()
const { setShowModerationSettingModal } = useModalContext()
const { locale } = useContext(I18n)
const {
moderationConfig,
setModerationConfig,
} = useContext(ConfigContext)
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)
const handleOpenModerationSettingModal = () => {
setShowModerationSettingModal({
payload: moderationConfig,
onSaveCallback: setModerationConfig,
})
}
const renderInfo = () => {
let prefix = ''
let suffix = ''
if (moderationConfig.type === 'openai_moderation')
prefix = t('appDebug.feature.moderation.modal.provider.openai')
else if (moderationConfig.type === 'keywords')
prefix = t('appDebug.feature.moderation.modal.provider.keywords')
else if (moderationConfig.type === 'api')
prefix = t('common.apiBasedExtension.selector.title')
else
prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[locale] || ''
if (moderationConfig.config?.inputs_config?.enabled && moderationConfig.config?.outputs_config?.enabled)
suffix = t('appDebug.feature.moderation.allEnabled')
else if (moderationConfig.config?.inputs_config?.enabled)
suffix = t('appDebug.feature.moderation.inputEnabled')
else if (moderationConfig.config?.outputs_config?.enabled)
suffix = t('appDebug.feature.moderation.outputEnabled')
return `${prefix} · ${suffix}`
}
return (
<div className='flex items-center px-3 h-12 bg-gray-50 rounded-xl overflow-hidden'>
<div className='shrink-0 flex items-center justify-center mr-1 w-6 h-6'>
<FileSearch02 className='shrink-0 w-4 h-4 text-[#039855]' />
</div>
<div className='shrink-0 mr-2 whitespace-nowrap text-sm text-gray-800 font-semibold'>
{t('appDebug.feature.moderation.title')}
</div>
<div
className='grow block w-0 text-right text-xs text-gray-500 truncate'
title={renderInfo()}>
{renderInfo()}
</div>
<div className='shrink-0 ml-4 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
<div
className={`
shrink-0 flex items-center px-3 h-7 cursor-pointer rounded-md
text-xs text-gray-700 font-medium hover:bg-gray-200
`}
onClick={handleOpenModerationSettingModal}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.operation.settings')}
</div>
</div>
)
}
export default Moderation

View File

@ -1,72 +0,0 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import type { ModerationContentConfig } from '@/models/debug'
type ModerationContentProps = {
title: string
info?: string
showPreset?: boolean
config: ModerationContentConfig
onConfigChange: (config: ModerationContentConfig) => void
}
const ModerationContent: FC<ModerationContentProps> = ({
title,
info,
showPreset = true,
config,
onConfigChange,
}) => {
const { t } = useTranslation()
const handleConfigChange = (field: string, value: boolean | string) => {
if (field === 'preset_response' && typeof value === 'string')
value = value.slice(0, 100)
onConfigChange({ ...config, [field]: value })
}
return (
<div className='py-2'>
<div className='rounded-lg bg-gray-50 border border-gray-200'>
<div className='flex items-center justify-between px-3 h-10 rounded-lg'>
<div className='shrink-0 text-sm font-medium text-gray-900'>{title}</div>
<div className='grow flex items-center justify-end'>
{
info && (
<div className='mr-2 text-xs text-gray-500 truncate' title={info}>{info}</div>
)
}
<Switch
size='l'
defaultValue={config.enabled}
onChange={v => handleConfigChange('enabled', v)}
/>
</div>
</div>
{
config.enabled && showPreset && (
<div className='px-3 pt-1 pb-3 bg-white rounded-lg'>
<div className='flex items-center justify-between h-8 text-[13px] font-medium text-gray-700'>
{t('appDebug.feature.moderation.modal.content.preset')}
<span className='text-xs font-normal text-gray-500'>{t('appDebug.feature.moderation.modal.content.supportMarkdown')}</span>
</div>
<div className='relative px-3 py-2 h-20 rounded-lg bg-gray-100'>
<textarea
value={config.preset_response || ''}
className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
placeholder={t('appDebug.feature.moderation.modal.content.placeholder') || ''}
onChange={e => handleConfigChange('preset_response', e.target.value)}
/>
<div className='absolute bottom-2 right-2 flex items-center px-1 h-5 rounded-md bg-gray-50 text-xs font-medium text-gray-300'>
<span>{(config.preset_response || '').length}</span>/<span className='text-gray-500'>100</span>
</div>
</div>
</div>
)
}
</div>
</div>
)
}
export default ModerationContent

View File

@ -1,373 +0,0 @@
import type { ChangeEvent, FC } from 'react'
import { useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import ModerationContent from './moderation-content'
import FormGeneration from './form-generation'
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
import { useToastContext } from '@/app/components/base/toast'
import {
fetchCodeBasedExtensionList,
fetchModelProviders,
} from '@/service/common'
import type { CodeBasedExtensionItem } from '@/models/common'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
const systemTypes = ['openai_moderation', 'keywords', 'api']
type Provider = {
key: string
name: string
form_schema?: CodeBasedExtensionItem['form_schema']
}
type ModerationSettingModalProps = {
data: ModerationConfig
onCancel: () => void
onSave: (moderationConfig: ModerationConfig) => void
}
const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
data,
onCancel,
onSave,
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { locale } = useContext(I18n)
const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
const { setShowAccountSettingModal } = useModalContext()
const handleOpenSettingsModal = () => {
setShowAccountSettingModal({
payload: 'provider',
onCancelCallback: () => {
mutate()
},
})
}
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)
const openaiProvider = modelProviders?.data.find(item => item.provider === 'openai')
const systemOpenaiProviderEnabled = openaiProvider?.system_configuration.enabled
const systemOpenaiProviderQuota = systemOpenaiProviderEnabled ? openaiProvider?.system_configuration.quota_configurations.find(item => item.quota_type === openaiProvider.system_configuration.current_quota_type) : undefined
const systemOpenaiProviderCanUse = systemOpenaiProviderQuota?.is_valid
const customOpenaiProvidersCanUse = openaiProvider?.custom_configuration.status === CustomConfigurationStatusEnum.active
const isOpenAIProviderConfigured = customOpenaiProvidersCanUse || systemOpenaiProviderCanUse
const providers: Provider[] = [
{
key: 'openai_moderation',
name: t('appDebug.feature.moderation.modal.provider.openai'),
},
{
key: 'keywords',
name: t('appDebug.feature.moderation.modal.provider.keywords'),
},
{
key: 'api',
name: t('common.apiBasedExtension.selector.title'),
},
...(
codeBasedExtensionList
? codeBasedExtensionList.data.map((item) => {
return {
key: item.name,
name: locale === 'zh-Hans' ? item.label['zh-Hans'] : item.label['en-US'],
form_schema: item.form_schema,
}
})
: []
),
]
const currentProvider = providers.find(provider => provider.key === localeData.type)
const handleDataTypeChange = (type: string) => {
let config: undefined | Record<string, any>
const currProvider = providers.find(provider => provider.key === type)
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
config = currProvider?.form_schema.reduce((prev, next) => {
prev[next.variable] = next.default
return prev
}, {} as Record<string, any>)
}
setLocaleData({
...localeData,
type,
config,
})
}
const handleDataKeywordsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value
const arr = value.split('\n').reduce((prev: string[], next: string) => {
if (next !== '')
prev.push(next.slice(0, 100))
if (next === '' && prev[prev.length - 1] !== '')
prev.push(next)
return prev
}, [])
setLocaleData({
...localeData,
config: {
...localeData.config,
keywords: arr.slice(0, 100).join('\n'),
},
})
}
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
setLocaleData({
...localeData,
config: {
...localeData.config,
[contentType]: contentConfig,
},
})
}
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
setLocaleData({
...localeData,
config: {
...localeData.config,
api_based_extension_id: apiBasedExtensionId,
},
})
}
const handleDataExtraChange = (extraValue: Record<string, string>) => {
setLocaleData({
...localeData,
config: {
...localeData.config,
...extraValue,
},
})
}
const formatData = (originData: ModerationConfig) => {
const { enabled, type, config } = originData
const { inputs_config, outputs_config } = config!
const params: Record<string, string | undefined> = {}
if (type === 'keywords')
params.keywords = config?.keywords
if (type === 'api')
params.api_based_extension_id = config?.api_based_extension_id
if (systemTypes.findIndex(t => t === type) < 0 && currentProvider?.form_schema) {
currentProvider.form_schema.forEach((form) => {
params[form.variable] = config?.[form.variable]
})
}
return {
type,
enabled,
config: {
inputs_config: inputs_config || { enabled: false },
outputs_config: outputs_config || { enabled: false },
...params,
},
}
}
const handleSave = () => {
if (localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
return
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.condition') })
return
}
if (localeData.type === 'keywords' && !localeData.config.keywords) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }) })
return
}
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) })
return
}
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
for (let i = 0; i < currentProvider.form_schema.length; i++) {
if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
notify({
type: 'error',
message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
})
return
}
}
}
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.errorMessage') })
return
}
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.errorMessage') })
return
}
onSave(formatData(localeData))
}
return (
<Modal
isShow
onClose={() => { }}
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
>
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
{t('appDebug.feature.moderation.modal.title')}
</div>
<div className='py-2'>
<div className='leading-9 text-sm font-medium text-gray-900'>
{t('appDebug.feature.moderation.modal.provider.title')}
</div>
<div className='grid gap-2.5 grid-cols-3'>
{
providers.map(provider => (
<div
key={provider.key}
className={`
flex items-center px-3 py-2 rounded-lg text-sm text-gray-900 cursor-pointer
${localeData.type === provider.key ? 'bg-white border-[1.5px] border-primary-400 shadow-sm' : 'border border-gray-100 bg-gray-25'}
${localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'opacity-50'}
`}
onClick={() => handleDataTypeChange(provider.key)}
>
<div className={`
mr-2 w-4 h-4 rounded-full border
${localeData.type === provider.key ? 'border-[5px] border-primary-600' : 'border border-gray-300'}`} />
{provider.name}
</div>
))
}
</div>
{
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
<div className='flex items-center mt-2 px-3 py-2 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'>
<InfoCircle className='mr-1 w-4 h-4 text-[#F79009]' />
<div className='flex items-center text-xs font-medium text-gray-700'>
{t('appDebug.feature.moderation.modal.openaiNotConfig.before')}
<span
className='text-primary-600 cursor-pointer'
onClick={handleOpenSettingsModal}
>
&nbsp;{t('common.settings.provider')}&nbsp;
</span>
{t('appDebug.feature.moderation.modal.openaiNotConfig.after')}
</div>
</div>
)
}
</div>
{
localeData.type === 'keywords' && (
<div className='py-2'>
<div className='mb-1 text-sm font-medium text-gray-900'>{t('appDebug.feature.moderation.modal.provider.keywords')}</div>
<div className='mb-2 text-xs text-gray-500'>{t('appDebug.feature.moderation.modal.keywords.tip')}</div>
<div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
<textarea
value={localeData.config?.keywords || ''}
onChange={handleDataKeywordsChange}
className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
placeholder={t('appDebug.feature.moderation.modal.keywords.placeholder') || ''}
/>
<div className='absolute bottom-2 right-2 flex items-center px-1 h-5 rounded-md bg-gray-50 text-xs font-medium text-gray-300'>
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>/<span className='text-gray-500'>100 {t('appDebug.feature.moderation.modal.keywords.line')}</span>
</div>
</div>
</div>
)
}
{
localeData.type === 'api' && (
<div className='py-2'>
<div className='flex items-center justify-between h-9'>
<div className='text-sm font-medium text-gray-900'>{t('common.apiBasedExtension.selector.title')}</div>
<a
href={t('common.apiBasedExtension.linkUrl') || '/'}
target='_blank' rel='noopener noreferrer'
className='group flex items-center text-xs text-gray-500 hover:text-primary-600'
>
<BookOpen01 className='mr-1 w-3 h-3 text-gray-500 group-hover:text-primary-600' />
{t('common.apiBasedExtension.link')}
</a>
</div>
<ApiBasedExtensionSelector
value={localeData.config?.api_based_extension_id || ''}
onChange={handleDataApiBasedChange}
/>
</div>
)
}
{
systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
/>
)
}
<div className='my-3 h-[1px] bg-gradient-to-r from-[#F3F4F6]'></div>
<ModerationContent
title={t('appDebug.feature.moderation.modal.content.input') || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
showPreset={!(localeData.type === 'api')}
/>
<ModerationContent
title={t('appDebug.feature.moderation.modal.content.output') || ''}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('outputs_config', config)}
info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
showPreset={!(localeData.type === 'api')}
/>
<div className='mt-1 mb-8 text-xs font-medium text-gray-500'>{t('appDebug.feature.moderation.modal.content.condition')}</div>
<div className='flex items-center justify-end'>
<Button
onClick={onCancel}
className='mr-2'
>
{t('common.operation.cancel')}
</Button>
<Button
variant='primary'
onClick={handleSave}
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
>
{t('common.operation.save')}
</Button>
</div>
</Modal>
)
}
export default ModerationSettingModal

View File

@ -1,38 +0,0 @@
import ReactSlider from 'react-slider'
import s from './style.module.css'
import cn from '@/utils/classnames'
type ISliderProps = {
className?: string
value: number
max?: number
min?: number
step?: number
disabled?: boolean
onChange: (value: number) => void
}
const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
return <ReactSlider
disabled={disabled}
value={isNaN(value) ? 0 : value}
min={min || 0}
max={max || 100}
step={step || 1}
className={cn(className, s.slider)}
thumbClassName={cn(s['slider-thumb'], 'top-[-7px] w-2 h-[18px] bg-white border !border-black/8 rounded-[36px] shadow-md cursor-pointer')}
trackClassName={s['slider-track']}
onChange={onChange}
renderThumb={(props, state) => (
<div {...props}>
<div className='relative w-full h-full'>
<div className='absolute top-[-16px] left-[50%] translate-x-[-50%] leading-[18px] text-xs font-medium text-gray-900'>
{(state.valueNow / 100).toFixed(2)}
</div>
</div>
</div>
)}
/>
}
export default Slider

View File

@ -1,20 +0,0 @@
.slider {
position: relative;
}
.slider.disabled {
opacity: 0.6;
}
.slider-thumb:focus {
outline: none;
}
.slider-track {
background-color: #528BFF;
height: 2px;
}
.slider-track-1 {
background-color: #E5E7EB;
}

View File

@ -1,46 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Slider from '@/app/components/app/configuration/toolbox/score-slider/base-slider'
type Props = {
className?: string
value: number
onChange: (value: number) => void
}
const ScoreSlider: FC<Props> = ({
className,
value,
onChange,
}) => {
const { t } = useTranslation()
return (
<div className={className}>
<div className='h-[1px] mt-[14px]'>
<Slider
max={100}
min={80}
step={1}
value={value}
onChange={onChange}
/>
</div>
<div className='mt-[10px] flex justify-between items-center leading-4 text-xs font-normal '>
<div className='flex space-x-1 text-[#00A286]'>
<div>0.8</div>
<div>·</div>
<div>{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}</div>
</div>
<div className='flex space-x-1 text-[#0057D8]'>
<div>1.0</div>
<div>·</div>
<div>{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}</div>
</div>
</div>
</div>
)
}
export default React.memo(ScoreSlider)

View File

@ -3,7 +3,7 @@ import { useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import FormGeneration from '../toolbox/moderation/form-generation'
import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import EmojiPicker from '@/app/components/base/emoji-picker'
@ -21,13 +21,13 @@ import { useToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
const systemTypes = ['api']
type ExternalDataToolModalProps = {
interface ExternalDataToolModalProps {
data: ExternalDataTool
onCancel: () => void
onSave: (externalDataTool: ExternalDataTool) => void
onValidateBeforeSave?: (externalDataTool: ExternalDataTool) => boolean
}
type Provider = {
interface Provider {
key: string
name: string
form_schema?: CodeBasedExtensionItem['form_schema']

View File

@ -19,6 +19,8 @@ import type { AppMode } from '@/types/app'
import { createApp } from '@/service/apps'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppIcon from '@/app/components/base/app-icon'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
@ -27,7 +29,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
type CreateAppDialogProps = {
interface CreateAppDialogProps {
show: boolean
onSuccess: () => void
onClose: () => void
@ -275,11 +277,11 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
size='large' className='cursor-pointer'
onClick={() => { setShowAppIconPicker(true) }}
/>
<input
<Input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
className='grow h-10'
/>
</div>
{showAppIconPicker && <AppIconPicker
@ -295,8 +297,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
{/* description */}
<div className='pt-2 px-8'>
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div>
<textarea
className='w-full px-3 py-2 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs h-[80px] resize-none'
<Textarea
className='resize-none'
placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
value={description}
onChange={e => setDescription(e.target.value)}

View File

@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import Uploader from './uploader'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import {
@ -21,7 +22,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
type CreateFromDSLModalProps = {
interface CreateFromDSLModalProps {
show: boolean
onSuccess?: () => void
onClose: () => void
@ -171,9 +172,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
currentTab === CreateFromDSLModalTab.FROM_URL && (
<div>
<div className='mb-1 system-md-semibold leading6'>DSL URL</div>
<input
<Input
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
className='px-2 w-full h-8 border border-components-input-border-active bg-components-input-bg-active rounded-lg outline-none appearance-none placeholder:text-components-input-text-placeholder system-sm-regular'
value={dslUrlValue}
onChange={e => setDslUrlValue(e.target.value)}
/>

View File

@ -6,13 +6,14 @@ import s from './style.module.css'
import cn from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import type { AppIconType } from '@/types/app'
export type DuplicateAppModalProps = {
export interface DuplicateAppModalProps {
appName: string
icon_type: AppIconType | null
icon: string
@ -87,10 +88,10 @@ const DuplicateAppModal = ({
background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
/>
<input
<Input
value={name}
onChange={e => setName(e.target.value)}
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
className='h-10'
/>
</div>
{isAppsFull && <AppsFull loc='app-duplicate-create' />}

View File

@ -8,11 +8,11 @@ import Log from '@/app/components/app/log'
import WorkflowLog from '@/app/components/app/workflow-log'
import Annotation from '@/app/components/app/annotation'
import Loading from '@/app/components/base/loading'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
import TabSlider from '@/app/components/base/tab-slider-plain'
import { useStore as useAppStore } from '@/app/components/app/store'
type Props = {
interface Props {
pageType: PageType
}
@ -41,7 +41,7 @@ const LogAnnotation: FC<Props> = ({
}
return (
<div className='pt-4 px-6 h-full flex flex-col'>
<div className='pt-3 px-6 h-full flex flex-col'>
{appDetail.mode !== 'workflow' && (
<TabSlider
className='shrink-0'

View File

@ -2,14 +2,13 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import useSWR from 'swr'
import dayjs from 'dayjs'
import { RiCalendarLine } from '@remixicon/react'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import type { QueryParam } from './index'
import { SimpleSelect } from '@/app/components/base/select'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'
import Sort from '@/app/components/base/sort'
import { fetchAnnotationsCount } from '@/service/log'
dayjs.extend(quarterOfYear)
@ -28,7 +27,7 @@ export const TIME_PERIOD_LIST = [
{ value: 'all', name: 'allTime' },
]
type IFilterProps = {
interface IFilterProps {
isChatMode?: boolean
appId: string
queryParams: QueryParam
@ -41,45 +40,44 @@ const Filter: FC<IFilterProps> = ({ isChatMode, appId, queryParams, setQueryPara
if (!data)
return null
return (
<div className='flex flex-row flex-wrap gap-2 items-center mb-4 text-gray-900 text-base'>
<SimpleSelect
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
className='mt-0 !w-40'
<div className='flex flex-row flex-wrap gap-2 items-center mb-2'>
<Chip
className='min-w-[150px]'
panelClassName='w-[270px]'
leftIcon={<RiCalendarLine className='h-4 w-4 text-text-secondary' />}
value={queryParams.period || 7}
onSelect={(item) => {
setQueryParams({ ...queryParams, period: item.value })
setQueryParams({ ...queryParams, period: item.value as string })
}}
defaultValue={queryParams.period} />
<div className="relative rounded-md">
<SimpleSelect
defaultValue={'all'}
className='!w-[300px]'
onSelect={
(item) => {
if (!item.value)
return
setQueryParams({ ...queryParams, annotation_status: item.value as string })
}
}
items={[{ value: 'all', name: t('appLog.filter.annotation.all') },
{ value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
{ value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') }]}
/>
</div>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[180px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search')!}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
onClear={() => setQueryParams({ ...queryParams, period: 7 })}
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
/>
<Chip
className='min-w-[150px]'
panelClassName='w-[270px]'
showLeftIcon={false}
value={queryParams.annotation_status || 'all'}
onSelect={(item) => {
setQueryParams({ ...queryParams, annotation_status: item.value as string })
}}
onClear={() => setQueryParams({ ...queryParams, annotation_status: 'all' })}
items={[
{ value: 'all', name: t('appLog.filter.annotation.all') },
{ value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
{ value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') },
]}
/>
<Input
wrapperClassName='w-[200px]'
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
/>
{isChatMode && (
<>
<div className='w-px h-3.5 bg-divider-regular'></div>

View File

@ -17,11 +17,11 @@ import Loading from '@/app/components/base/loading'
import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
import { APP_PAGE_LIMIT } from '@/config'
import type { App, AppMode } from '@/types/app'
export type ILogsProps = {
export interface ILogsProps {
appDetail: App
}
export type QueryParam = {
export interface QueryParam {
period?: number | string
annotation_status?: string
keyword?: string
@ -40,12 +40,12 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
const pathSegments = pathname.split('/')
pathSegments.pop()
return <div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-text-secondary system-md-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-text-tertiary system-sm-regular'>
<Trans
i18nKey="appLog.table.empty.element.content"
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-util-colors-blue-blue-600' />, testLink: <Link href={appUrl} className='text-util-colors-blue-blue-600' target='_blank' rel='noopener noreferrer' /> }}
/>
</div>
</div>
@ -103,7 +103,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
return (
<div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
{total === undefined

View File

@ -16,12 +16,11 @@ import timezone from 'dayjs/plugin/timezone'
import { createContext, useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import { UUID_NIL } from '../../base/chat/constants'
import s from './style.module.css'
import type { ChatItemInTree } from '../../base/chat/types'
import VarPanel from './var-panel'
import cn from '@/utils/classnames'
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
import type { App } from '@/types/app'
import Loading from '@/app/components/base/loading'
import Drawer from '@/app/components/base/drawer'
@ -42,11 +41,13 @@ import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
import Tooltip from '@/app/components/base/tooltip'
import { CopyIcon } from '@/app/components/base/copy-icon'
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
dayjs.extend(utc)
dayjs.extend(timezone)
type IConversationList = {
interface IConversationList {
logs?: ChatConversationsResponse | CompletionConversationsResponse
appDetail: App
onRefresh: () => void
@ -54,7 +55,7 @@ type IConversationList = {
const defaultValue = 'N/A'
type IDrawerContext = {
interface IDrawerContext {
onClose: () => void
appDetail?: App
}
@ -82,104 +83,88 @@ const PARAM_MAP = {
frequency_penalty: 'Frequency Penalty',
}
function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) {
newChatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedbacks.find((item: any) => item.from_source === 'user'), // user feedback
adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback
feedbackDisabled: false,
isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
log: [
...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant'
? [
{
role: 'assistant',
text: item.answer,
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
workflow_run_id: item.workflow_run_id,
conversationId,
input: {
inputs: item.inputs,
query: item.query,
},
more: {
time: dayjs.unix(item.created_at).tz(timezone).format(format),
tokens: item.answer_tokens + item.message_tokens,
latency: item.provider_response_latency.toFixed(2),
},
citation: item.metadata?.retriever_resources,
annotation: (() => {
if (item.annotation_hit_history) {
return {
id: item.annotation_hit_history.annotation_id,
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
created_at: item.annotation_hit_history.created_at,
}
}
if (item.annotation) {
return {
id: item.annotation.id,
authorName: item.annotation.account.name,
logAnnotation: item.annotation,
created_at: 0,
}
}
return undefined
})(),
parentMessageId: `question-${item.id}`,
})
newChatList.push({
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
parentMessageId: item.parent_message_id || undefined,
})
}
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
const newChatList: IChatItem[] = []
let nextMessageId = null
for (const item of messages) {
if (!item.parent_message_id) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
break
}
messages.forEach((item: ChatMessage) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: item.parent_message_id || undefined,
})
if (!nextMessageId) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
nextMessageId = item.parent_message_id
}
else {
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
nextMessageId = item.parent_message_id
}
}
}
return newChatList.reverse()
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
newChatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
feedbackDisabled: false,
isAnswer: true,
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
log: [
...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant'
? [
{
role: 'assistant',
text: item.answer,
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
] as IChatItem['log'],
workflow_run_id: item.workflow_run_id,
conversationId,
input: {
inputs: item.inputs,
query: item.query,
},
more: {
time: dayjs.unix(item.created_at).tz(timezone).format(format),
tokens: item.answer_tokens + item.message_tokens,
latency: item.provider_response_latency.toFixed(2),
},
citation: item.metadata?.retriever_resources,
annotation: (() => {
if (item.annotation_hit_history) {
return {
id: item.annotation_hit_history.annotation_id,
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
created_at: item.annotation_hit_history.created_at,
}
}
if (item.annotation) {
return {
id: item.annotation.id,
authorName: item.annotation.account.name,
logAnnotation: item.annotation,
created_at: 0,
}
}
return undefined
})(),
parentMessageId: `question-${item.id}`,
})
})
return newChatList
}
// const displayedParams = CompletionParams.slice(0, -2)
const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
type IDetailPanel<T> = {
interface IDetailPanel {
detail: any
onFeedback: FeedbackFunc
onSubmitAnnotation: SubmitAnnotationFunc
}
function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
@ -191,50 +176,66 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))
const { t } = useTranslation()
const [items, setItems] = React.useState<IChatItem[]>([])
const fetchedMessages = useRef<ChatMessage[]>([])
const [hasMore, setHasMore] = useState(true)
const [varValues, setVarValues] = useState<Record<string, string>>({})
const fetchData = async () => {
const [allChatItems, setAllChatItems] = useState<IChatItem[]>([])
const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
const fetchData = useCallback(async () => {
try {
if (!hasMore)
return
const params: ChatMessagesRequest = {
conversation_id: detail.id,
limit: 10,
}
if (items?.[0]?.id)
params.first_id = items?.[0]?.id.replace('question-', '')
if (allChatItems.at(-1)?.id)
params.first_id = allChatItems.at(-1)?.id.replace('question-', '')
const messageRes = await fetchChatMessages({
url: `/apps/${appDetail?.id}/chat-messages`,
params,
})
if (messageRes.data.length > 0) {
const varValues = messageRes.data[0].inputs
const varValues = messageRes.data.at(-1)!.inputs
setVarValues(varValues)
}
fetchedMessages.current = [...fetchedMessages.current, ...messageRes.data]
const newItems = getFormattedChatList(fetchedMessages.current, detail.id, timezone!, t('appLog.dateTimeFormat') as string)
setHasMore(messageRes.has_more)
const newAllChatItems = [
...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string),
...allChatItems,
]
setAllChatItems(newAllChatItems)
let tree = buildChatItemTree(newAllChatItems)
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
newItems.unshift({
tree = [{
id: 'introduction',
isAnswer: true,
isOpeningStatement: true,
content: detail?.model_config?.configs?.introduction ?? 'hello',
feedbackDisabled: true,
})
children: tree,
}]
}
setItems(newItems)
setHasMore(messageRes.has_more)
setChatItemTree(tree)
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
}
catch (err) {
console.error(err)
}
}
}, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
const switchSibling = useCallback((siblingMessageId: string) => {
setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId))
}, [chatItemTree])
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
setItems(items.map((item, i) => {
setAllChatItems(allChatItems.map((item, i) => {
if (i === index - 1) {
return {
...item,
@ -255,9 +256,9 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
}
return item
}))
}, [items])
}, [allChatItems])
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
setItems(items.map((item, i) => {
setAllChatItems(allChatItems.map((item, i) => {
if (i === index - 1) {
return {
...item,
@ -285,9 +286,9 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
}
return item
}))
}, [items])
}, [allChatItems])
const handleAnnotationRemoved = useCallback((index: number) => {
setItems(items.map((item, i) => {
setAllChatItems(allChatItems.map((item, i) => {
if (i === index) {
return {
...item,
@ -297,7 +298,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
}
return item
}))
}, [items])
}, [allChatItems])
const fetchInitiated = useRef(false)
@ -462,7 +463,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
siteInfo={null}
/>
</div>
: (items.length < 8 && !hasMore)
: threadChatItems.length < 8
? <div className="pt-4 mb-4">
<Chat
config={{
@ -476,7 +477,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
},
supportFeedback: true,
} as any}
chatList={items}
chatList={threadChatItems}
onAnnotationAdded={handleAnnotationAdded}
onAnnotationEdited={handleAnnotationEdited}
onAnnotationRemoved={handleAnnotationRemoved}
@ -485,6 +486,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
showPromptLog
hideProcessDetail
chatContainerInnerClassName='px-6'
switchSibling={switchSibling}
/>
</div>
: <div
@ -499,7 +501,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
{/* Put the scroll bar always on the bottom */}
<InfiniteScroll
scrollableTarget="scrollableDiv"
dataLength={items.length}
dataLength={threadChatItems.length}
next={fetchData}
hasMore={hasMore}
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
@ -530,7 +532,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
},
supportFeedback: true,
} as any}
chatList={items}
chatList={threadChatItems}
onAnnotationAdded={handleAnnotationAdded}
onAnnotationEdited={handleAnnotationEdited}
onAnnotationRemoved={handleAnnotationRemoved}
@ -539,6 +541,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
showPromptLog
hideProcessDetail
chatContainerInnerClassName='px-6'
switchSibling={switchSibling}
/>
</InfiniteScroll>
</div>
@ -597,7 +600,7 @@ const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: st
if (!conversationDetail)
return null
return <DetailPanel<CompletionConversationFullDetailResponse>
return <DetailPanel
detail={conversationDetail}
onFeedback={handleFeedback}
onSubmitAnnotation={handleAnnotation}
@ -640,7 +643,7 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
if (!conversationDetail)
return null
return <DetailPanel<ChatConversationFullDetailResponse>
return <DetailPanel
detail={conversationDetail}
onFeedback={handleFeedback}
onSubmitAnnotation={handleAnnotation}
@ -666,13 +669,13 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
return (
<Tooltip
popupContent={
<span className='text-xs text-gray-500 inline-flex items-center'>
<span className='text-xs text-text-tertiary inline-flex items-center'>
<RiEditFill className='w-3 h-3 mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
</span>
}
popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'}
>
<div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
<div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap')}>
{value || '-'}
</div>
</Tooltip>
@ -690,40 +693,46 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
return (
<div className='overflow-x-auto'>
<table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
<thead className='system-xs-medium-uppercase text-text-tertiary'>
<tr>
<td className='w-[1.375rem] whitespace-nowrap'></td>
<td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
<td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.time')}</td>
</tr>
</thead>
<tbody className="text-gray-500">
<tbody className="text-text-secondary system-sm-regular">
{logs.data.map((log: any) => {
const endUser = log.from_end_user_session_id || log.from_account_name
const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
return <tr
key={log.id}
className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentConversation?.id !== log.id ? '' : 'bg-gray-50'}`}
className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentConversation?.id !== log.id ? '' : 'bg-background-default-hover')}
onClick={() => {
setShowDrawer(true)
setCurrentConversation(log)
}}>
<td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
<td style={{ maxWidth: isChatMode ? 300 : 200 }}>
<td className='h-4'>
{!log.read_at && (
<div className='p-3 pr-0.5 flex items-center'>
<span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
</div>
)}
</td>
<td className='p-3 pr-2 w-[160px]' style={{ maxWidth: isChatMode ? 300 : 200 }}>
{renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
</td>
<td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
<td style={{ maxWidth: isChatMode ? 100 : 200 }}>
<td className='p-3 pr-2'>{renderTdValue(endUser || defaultValue, !endUser)}</td>
<td className='p-3 pr-2' style={{ maxWidth: isChatMode ? 100 : 200 }}>
{renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
</td>
<td>
<td className='p-3 pr-2'>
{(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
? renderTdValue(defaultValue, true)
: <>
@ -732,7 +741,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
</>
}
</td>
<td>
<td className='p-3 pr-2'>
{(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
? renderTdValue(defaultValue, true)
: <>
@ -741,8 +750,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
</>
}
</td>
<td className='w-[160px]'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='w-[160px] p-3 pr-2'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='w-[160px] p-3 pr-2'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
</tr>
})}
</tbody>

View File

@ -1,9 +1,3 @@
.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}

View File

@ -8,6 +8,8 @@ import { useContextSelector } from 'use-context-selector'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppIcon from '@/app/components/base/app-icon'
import Switch from '@/app/components/base/switch'
import { SimpleSelect } from '@/app/components/base/select'
@ -20,7 +22,7 @@ import AppContext, { useAppContext } from '@/context/app-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
export type ISettingsModalProps = {
export interface ISettingsModalProps {
isChat: boolean
appInfo: AppDetailResponse & Partial<AppSSO>
isShow: boolean
@ -29,7 +31,7 @@ export type ISettingsModalProps = {
onSave?: (params: ConfigParams) => Promise<void>
}
export type ConfigParams = {
export interface ConfigParams {
title: string
description: string
default_language: string
@ -183,6 +185,10 @@ const SettingsModal: FC<ISettingsModalProps> = ({
}
}
const onDesChange = (value: string) => {
setInputInfo(item => ({ ...item, desc: value }))
}
return (
<>
<Modal
@ -201,7 +207,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
/>
<input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='grow h-10'
value={inputInfo.title}
onChange={onChange('title')}
placeholder={t('app.appNamePlaceholder') || ''}
@ -209,11 +216,10 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
<textarea
rows={3}
className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
<Textarea
className='mt-2'
value={inputInfo.desc}
onChange={onChange('desc')}
onChange={e => onDesChange(e.target.value)}
placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
/>
{isChatBot && (
@ -249,7 +255,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
{isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.chatColorTheme ?? ''}
onChange={onChange('chatColorTheme')}
placeholder='E.g #A020F0'
@ -283,7 +290,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
{isShowMore && <>
<hr className='w-full mt-6' />
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.copyright}
onChange={onChange('copyright')}
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
@ -295,14 +303,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
/>
</p>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
/>
<div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.customDisclaimer}
onChange={onChange('customDisclaimer')}
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string}

View File

@ -15,9 +15,4 @@
.policy {
font-size: 0.75rem;
line-height: 1.125rem;
}
.projectName {
font-size: 0.875rem;
line-height: 2.5rem;
}

View File

@ -2,7 +2,7 @@ import { create } from 'zustand'
import type { App, AppSSO } from '@/types/app'
import type { IChatItem } from '@/app/components/base/chat/chat/type'
type State = {
interface State {
appDetail?: App & Partial<AppSSO>
appSidebarExpand: string
currentLogItem?: IChatItem
@ -10,9 +10,10 @@ type State = {
showPromptLogModal: boolean
showAgentLogModal: boolean
showMessageLogModal: boolean
showAppConfigureFeaturesModal: boolean
}
type Action = {
interface Action {
setAppDetail: (appDetail?: App & Partial<AppSSO>) => void
setAppSiderbarExpand: (state: string) => void
setCurrentLogItem: (item?: IChatItem) => void
@ -20,6 +21,7 @@ type Action = {
setShowPromptLogModal: (showPromptLogModal: boolean) => void
setShowAgentLogModal: (showAgentLogModal: boolean) => void
setShowMessageLogModal: (showMessageLogModal: boolean) => void
setShowAppConfigureFeaturesModal: (showAppConfigureFeaturesModal: boolean) => void
}
export const useStore = create<State & Action>(set => ({
@ -47,4 +49,6 @@ export const useStore = create<State & Action>(set => ({
}
}
}),
showAppConfigureFeaturesModal: false,
setShowAppConfigureFeaturesModal: showAppConfigureFeaturesModal => set(() => ({ showAppConfigureFeaturesModal })),
}))

View File

@ -8,7 +8,9 @@ import { RiCloseLine } from '@remixicon/react'
import AppIconPicker from '../../base/app-icon-picker'
import s from './style.module.css'
import cn from '@/utils/classnames'
import Checkbox from '@/app/components/base/checkbox'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
@ -23,7 +25,7 @@ import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/aler
import AppIcon from '@/app/components/base/app-icon'
import { useStore as useAppStore } from '@/app/components/app/store'
type SwitchAppModalProps = {
interface SwitchAppModalProps {
show: boolean
appDetail: App
onSuccess?: () => void
@ -121,11 +123,11 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
/>
<input
<Input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
className='grow h-10'
/>
</div>
{showAppIconPicker && <AppIconPicker
@ -144,8 +146,8 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
{isAppsFull && <AppsFull loc='app-switch' />}
<div className='pt-6 flex justify-between items-center'>
<div className='flex items-center'>
<input id="removeOriginal" type="checkbox" checked={removeOriginal} onChange={() => setRemoveOriginal(!removeOriginal)} className="w-4 h-4 rounded border-gray-300 text-blue-700 cursor-pointer focus:ring-blue-700" />
<label htmlFor="removeOriginal" className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer">{t('app.removeOriginal')}</label>
<Checkbox className='shrink-0' checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
<div className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer" onClick={() => setRemoveOriginal(!removeOriginal)}>{t('app.removeOriginal')}</div>
</div>
<div className='flex items-center'>
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>

View File

@ -22,8 +22,8 @@ import { File02 } from '@/app/components/base/icons/src/vender/line/files'
import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
import { fetchTextGenerationMessage } from '@/service/debug'
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
@ -33,7 +33,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context'
const MAX_DEPTH = 3
export type IGenerationItemProps = {
export interface IGenerationItemProps {
isWorkflow?: boolean
workflowProcessData?: WorkflowProcess
className?: string
@ -307,7 +307,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<div className={`flex ${contentClassName}`}>
<div className='grow w-0'>
{siteInfo && siteInfo.show_workflow_steps && workflowProcessData && (
<WorkflowProcessItem grayBg hideInfo data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
<WorkflowProcessItem data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
)}
{workflowProcessData && !isError && (
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />

View File

@ -4,12 +4,11 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
// import Loading from '@/app/components/base/loading'
import { Markdown } from '@/app/components/base/markdown'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
// import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { FileList } from '@/app/components/base/file-uploader'
const ResultTab = ({
data,
@ -56,16 +55,28 @@ const ResultTab = ({
)}
<div className={cn('grow bg-white')}>
{currentTab === 'RESULT' && (
<Markdown content={data?.resultText || ''} />
<>
<Markdown content={data?.resultText || ''} />
{!!data?.files?.length && (
<FileList
files={data?.files}
showDeleteAction={false}
showDownloadAction
canPreview
/>
)}
</>
)}
{currentTab === 'DETAIL' && content && (
<CodeEditor
readOnly
title={<div>JSON OUTPUT</div>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
<div className='mt-1'>
<CodeEditor
readOnly
title={<div>JSON OUTPUT</div>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
</div>
)}
</div>
</div>

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import Run from '@/app/components/workflow/run'
type ILogDetail = {
interface ILogDetail {
runID: string
onClose: () => void
}
@ -13,11 +13,11 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose }) => {
const { t } = useTranslation()
return (
<div className='grow relative flex flex-col py-3'>
<div className='grow relative flex flex-col pt-3'>
<span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onClose}>
<RiCloseLine className='w-4 h-4 text-gray-500' />
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</span>
<h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.workflowTitle')}</h1>
<h1 className='shrink-0 px-4 py-1 text-text-primary system-xl-semibold'>{t('appLog.runDetail.workflowTitle')}</h1>
<Run runID={runID}/>
</div>
)

View File

@ -2,13 +2,11 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import type { QueryParam } from './index'
import { SimpleSelect } from '@/app/components/base/select'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'
type IFilterProps = {
interface IFilterProps {
queryParams: QueryParam
setQueryParams: (v: QueryParam) => void
}
@ -16,40 +14,30 @@ type IFilterProps = {
const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps) => {
const { t } = useTranslation()
return (
<div className='flex flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
<div className="relative rounded-md">
<SimpleSelect
defaultValue={'all'}
className='!min-w-[100px]'
onSelect={
(item) => {
if (!item.value)
return
setQueryParams({ ...queryParams, status: item.value as string })
}
}
items={[{ value: 'all', name: 'All' },
{ value: 'succeeded', name: 'Success' },
{ value: 'failed', name: 'Fail' },
{ value: 'stopped', name: 'Stop' },
]}
/>
</div>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search')!}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
<div className='flex flex-row flex-wrap gap-2 mb-2'>
<Chip
value={queryParams.status || 'all'}
onSelect={(item) => {
setQueryParams({ ...queryParams, status: item.value as string })
}}
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
items={[{ value: 'all', name: 'All' },
{ value: 'succeeded', name: 'Success' },
{ value: 'failed', name: 'Fail' },
{ value: 'stopped', name: 'Stop' },
]}
/>
<Input
wrapperClassName='w-[200px]'
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
/>
</div>
)
}

View File

@ -16,11 +16,11 @@ import { fetchWorkflowLogs } from '@/service/log'
import { APP_PAGE_LIMIT } from '@/config'
import type { App, AppMode } from '@/types/app'
export type ILogsProps = {
export interface ILogsProps {
appDetail: App
}
export type QueryParam = {
export interface QueryParam {
status?: string
keyword?: string
}
@ -36,12 +36,12 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
const pathSegments = pathname.split('/')
pathSegments.pop()
return <div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-text-secondary system-md-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-text-tertiary system-sm-regular'>
<Trans
i18nKey="appLog.table.empty.element.content"
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-util-colors-blue-blue-600' />, testLink: <Link href={appUrl} className='text-util-colors-blue-blue-600' target='_blank' rel='noopener noreferrer' /> }}
/>
</div>
</div>
@ -75,8 +75,8 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
return (
<div className='flex flex-col h-full'>
<h1 className='text-md font-semibold text-gray-900'>{t('appLog.workflowTitle')}</h1>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.workflowSubtitle')}</p>
<h1 className='text-text-primary system-xl-semibold'>{t('appLog.workflowTitle')}</h1>
<p className='text-text-tertiary system-sm-regular'>{t('appLog.workflowSubtitle')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter queryParams={queryParams} setQueryParams={setQueryParams} />
{/* workflow log */}

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
// import s from './style.module.css'
import DetailPanel from './detail'
import cn from '@/utils/classnames'
import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log'
@ -13,7 +13,7 @@ import Indicator from '@/app/components/header/indicator'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useTimestamp from '@/hooks/use-timestamp'
type ILogs = {
interface ILogs {
logs?: WorkflowLogsResponse
appDetail?: App
onRefresh: () => void
@ -34,33 +34,33 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
const statusTdRender = (status: string) => {
if (status === 'succeeded') {
return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'green'} />
<span>Success</span>
<span className='text-util-colors-green-green-600'>Success</span>
</div>
)
}
if (status === 'failed') {
return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'red'} />
<span className='text-red-600'>Fail</span>
<span className='text-util-colors-red-red-600'>Fail</span>
</div>
)
}
if (status === 'stopped') {
return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'yellow'} />
<span>Stop</span>
<span className='text-util-colors-warning-warning-600'>Stop</span>
</div>
)
}
if (status === 'running') {
return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'blue'} />
<span className='text-primary-600'>Running</span>
<span className='text-util-colors-blue-light-blue-light-600'>Running</span>
</div>
)
}
@ -77,43 +77,47 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
return (
<div className='overflow-x-auto'>
<table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
<thead className="h-8 !pl-3 py-2 leading-[18px] border-b border-gray-200 text-xs text-gray-500 font-medium">
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
<thead className='system-xs-medium-uppercase text-text-tertiary'>
<tr>
<td className='w-[1.375rem] whitespace-nowrap'></td>
<td className='whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.status')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.user')}</td>
{/* <td className='whitespace-nowrap'>{t('appLog.table.header.version')}</td> */}
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.status')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.user')}</td>
</tr>
</thead>
<tbody className="text-gray-700 text-[13px]">
<tbody className="text-text-secondary system-sm-regular">
{logs.data.map((log: WorkflowAppLogDetail) => {
const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : log.created_by_account ? log.created_by_account.name : defaultValue
return <tr
key={log.id}
className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentLog?.id !== log.id ? '' : 'bg-gray-50'}`}
className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentLog?.id !== log.id ? '' : 'bg-background-default-hover')}
onClick={() => {
setCurrentLog(log)
setShowDrawer(true)
}}>
<td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
<td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td>{statusTdRender(log.workflow_run.status)}</td>
<td>
<td className='h-4'>
{!log.read_at && (
<div className='p-3 pr-0.5 flex items-center'>
<span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
</div>
)}
</td>
<td className='p-3 pr-2 w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='p-3 pr-2'>{statusTdRender(log.workflow_run.status)}</td>
<td className='p-3 pr-2'>
<div className={cn(
log.workflow_run.elapsed_time === 0 && 'text-gray-400',
log.workflow_run.elapsed_time === 0 && 'text-text-quaternary',
)}>{`${log.workflow_run.elapsed_time.toFixed(3)}s`}</div>
</td>
<td>{log.workflow_run.total_tokens}</td>
<td>
<div className={cn(endUser === defaultValue ? 'text-gray-400' : 'text-gray-700', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
<td className='p-3 pr-2'>{log.workflow_run.total_tokens}</td>
<td className='p-3 pr-2'>
<div className={cn(endUser === defaultValue ? 'text-text-quaternary' : 'text-text-secondary', 'overflow-hidden text-ellipsis whitespace-nowrap')}>
{endUser}
</div>
</td>
{/* <td>VERSION</td> */}
</tr>
})}
</tbody>
@ -123,7 +127,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
onClose={onCloseDrawer}
mask={isMobile}
footer={null}
panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-gray-200'
panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-components-panel-border'
>
<DetailPanel onClose={onCloseDrawer} runID={currentLog?.workflow_run.id || ''} />
</Drawer>

View File

@ -1,9 +1,3 @@
.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}

View File

@ -1,14 +1,13 @@
'use client'
import { useRef, useState } from 'react'
import { useState } from 'react'
import { t } from 'i18next'
import { useParams, usePathname } from 'next/navigation'
import s from './style.module.css'
import Tooltip from '@/app/components/base/tooltip'
import { randomString } from '@/utils'
import Loading from '@/app/components/base/loading'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
type AudioBtnProps = {
interface AudioBtnProps {
id?: string
voice?: string
value?: string
@ -28,7 +27,6 @@ const AudioBtn = ({
}: AudioBtnProps) => {
const [audioState, setAudioState] = useState<AudioState>('initial')
const selector = useRef(`play-tooltip-${randomString(4)}`)
const params = useParams()
const pathname = usePathname()
const audio_finished_call = (event: string): any => {

View File

@ -4,7 +4,7 @@ import React from 'react'
import { RiAddLine } from '@remixicon/react'
import cn from '@/utils/classnames'
type Props = {
interface Props {
className?: string
onClick: () => void
}
@ -14,8 +14,8 @@ const AddButton: FC<Props> = ({
onClick,
}) => {
return (
<div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-gray-200 select-none')} onClick={onClick}>
<RiAddLine className='w-4 h-4 text-gray-500' />
<div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-state-base-hover select-none')} onClick={onClick}>
<RiAddLine className='w-4 h-4 text-text-tertiary' />
</div>
)
}

View File

@ -40,6 +40,10 @@ const ChatWrapper = () => {
return {
...config,
file_upload: {
...(config as any).file_upload,
fileUploadConfig: (config as any).system_parameters,
},
supportFeedback: true,
opening_statement: currentConversationId ? currentConversationItem?.introduction : (config as any).opening_statement,
} as ChatConfig
@ -56,7 +60,7 @@ const ChatWrapper = () => {
appConfig,
{
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
promptVariables: inputsForms,
inputsForm: inputsForms,
},
appPrevChatList,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
@ -65,19 +69,18 @@ const ChatWrapper = () => {
useEffect(() => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const doSend: OnSend = useCallback((message, files, last_answer) => {
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
}
if (appConfig?.file_upload?.image.enabled && files?.length)
data.files = files
handleSend(
getUrl('chat-messages', isInstalledApp, appId || ''),
data,
@ -89,7 +92,6 @@ const ChatWrapper = () => {
)
}, [
chatListRef,
appConfig,
currentConversationId,
currentConversationItem,
handleSend,
@ -162,25 +164,31 @@ const ChatWrapper = () => {
: null
return (
<Chat
appData={appData}
config={appConfig}
chatList={chatList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-full ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
/>
<div
className='h-full bg-chatbot-bg overflow-hidden'
>
<Chat
appData={appData}
config={appConfig}
chatList={chatList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
/>
</div>
)
}

View File

@ -1,8 +1,9 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import Textarea from '@/app/components/base/textarea'
type InputProps = {
interface InputProps {
form: any
value: string
onChange: (variable: string, value: string) => void
@ -23,9 +24,9 @@ const FormInput: FC<InputProps> = ({
if (type === 'paragraph') {
return (
<textarea
<Textarea
value={value}
className='grow h-[104px] rounded-lg bg-gray-100 px-2.5 py-2 outline-none appearance-none resize-none'
className='resize-none'
onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>

View File

@ -3,22 +3,26 @@ import { useTranslation } from 'react-i18next'
import { useChatWithHistoryContext } from '../context'
import Input from './form-input'
import { PortalSelect } from '@/app/components/base/select'
import { InputVarType } from '@/app/components/workflow/types'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
const Form = () => {
const { t } = useTranslation()
const {
appParams,
inputsForms,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
isMobile,
} = useChatWithHistoryContext()
const handleFormChange = useCallback((variable: string, value: string) => {
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputs,
...newConversationInputsRef.current,
[variable]: value,
})
}, [newConversationInputs, handleNewConversationInputsChange])
}, [newConversationInputsRef, handleNewConversationInputsChange])
const renderField = (form: any) => {
const {
@ -48,6 +52,36 @@ const Form = () => {
/>
)
}
if (form.type === InputVarType.singleFile) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
onChange={files => handleFormChange(variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
fileUploadConfig: (appParams as any).system_parameters,
}}
/>
)
}
return (
<PortalSelect

View File

@ -16,7 +16,7 @@ import type {
ConversationItem,
} from '@/models/share'
export type ChatWithHistoryContextValue = {
export interface ChatWithHistoryContextValue {
appInfoError?: any
appInfoLoading?: boolean
appMeta?: AppMeta
@ -30,6 +30,7 @@ export type ChatWithHistoryContextValue = {
conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean
newConversationInputs: Record<string, any>
newConversationInputsRef: RefObject<Record<string, any>>
handleNewConversationInputsChange: (v: Record<string, any>) => void
inputsForms: any[]
handleNewConversation: () => void
@ -57,6 +58,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
conversationList: [],
showConfigPanelBeforeChat: false,
newConversationInputs: {},
newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {},
inputsForms: [],
handleNewConversation: () => {},

View File

@ -37,6 +37,8 @@ import type {
import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config'
import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
@ -127,7 +129,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setNewConversationInputs(newInputs)
}, [])
const inputsForms = useMemo(() => {
return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input'] || item.number).map((item: any) => {
return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => {
if (item.paragraph) {
return {
...item.paragraph,
@ -147,6 +149,20 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
}
if (item['file-list']) {
return {
...item['file-list'],
type: 'file-list',
}
}
if (item.file) {
return {
...item.file,
type: 'file',
}
}
return {
...item['text-input'],
type: 'text-input',
@ -206,21 +222,38 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { notify } = useToastContext()
const checkInputsRequired = useCallback((silent?: boolean) => {
if (inputsForms.length) {
for (let i = 0; i < inputsForms.length; i += 1) {
const item = inputsForms[i]
if (item.required && !newConversationInputsRef.current[item.variable]) {
if (!silent) {
notify({
type: 'error',
message: t('appDebug.errorMessage.valueOfVarRequired', { key: item.variable }),
})
}
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
return
if (fileIsUploading)
return
if (!newConversationInputsRef.current[variable] && !silent)
hasEmptyInput = label as string
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) {
const files = newConversationInputsRef.current[variable]
if (Array.isArray(files))
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
else
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
}
}
return true
})
}
if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
return false
}
if (fileIsUploading) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
return
}
return true
@ -377,6 +410,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setShowConfigPanelBeforeChat,
setShowNewConversationItemInList,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
inputsForms,
handleNewConversation,

View File

@ -20,7 +20,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
type ChatWithHistoryProps = {
interface ChatWithHistoryProps {
className?: string
}
const ChatWithHistory: FC<ChatWithHistoryProps> = ({
@ -99,7 +99,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
)
}
export type ChatWithHistoryWrapProps = {
export interface ChatWithHistoryWrapProps {
installedAppInfo?: InstalledApp
className?: string
}
@ -125,6 +125,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
conversationList,
showConfigPanelBeforeChat,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
inputsForms,
handleNewConversation,
@ -158,6 +159,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
conversationList,
showConfigPanelBeforeChat,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
inputsForms,
handleNewConversation,

Some files were not shown because too many files have changed in this diff Show More