feat: advanced prompt (#1330)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: Gillian97 <jinling.sunshine@gmail.com>
This commit is contained in:
zxhlyh
2023-10-12 23:14:28 +08:00
committed by GitHub
parent 42a5b3ec17
commit 5b9858a8a3
131 changed files with 5944 additions and 451 deletions

View File

@@ -53,6 +53,7 @@ export type IChatProps = {
isShowConfigElem?: boolean
dataSets?: DataSet[]
isShowCitationHitInfo?: boolean
isShowPromptLog?: boolean
}
const Chat: FC<IChatProps> = ({
@@ -81,6 +82,7 @@ const Chat: FC<IChatProps> = ({
isShowConfigElem,
dataSets,
isShowCitationHitInfo,
isShowPromptLog,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
@@ -186,7 +188,18 @@ const Chat: FC<IChatProps> = ({
isShowCitationHitInfo={isShowCitationHitInfo}
/>
}
return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />
return (
<Question
key={item.id}
id={item.id}
content={item.content}
more={item.more}
useCurrentUserAvatar={useCurrentUserAvatar}
item={item}
isShowPromptLog={isShowPromptLog}
isResponsing={isResponsing}
/>
)
})}
</div>
{

View File

@@ -0,0 +1,70 @@
import type { Dispatch, FC, ReactNode, RefObject, SetStateAction } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import Tooltip from '@/app/components/base/tooltip'
export type LogData = {
role: string
text: string
}
type LogProps = {
containerRef: RefObject<HTMLElement>
log: LogData[]
children?: (v: Dispatch<SetStateAction<boolean>>) => ReactNode
}
const Log: FC<LogProps> = ({
containerRef,
children,
log,
}) => {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const [width, setWidth] = useState(0)
const adjustModalWidth = () => {
if (containerRef.current)
setWidth(document.body.clientWidth - (containerRef.current?.clientWidth + 56 + 16))
}
useEffect(() => {
adjustModalWidth()
}, [])
return (
<>
{
children
? children(setShowModal)
: (
<Tooltip selector='prompt-log-modal-trigger' content={t('common.operation.log') || ''}>
<div className={`
hidden absolute -left-[14px] -top-[14px] group-hover:block w-7 h-7
p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-md cursor-pointer
`}>
<div
className='flex items-center justify-center rounded-md w-full h-full hover:bg-gray-100'
onClick={() => setShowModal(true)}
>
<File02 className='w-4 h-4 text-gray-500' />
</div>
</div>
</Tooltip>
)
}
{
showModal && (
<PromptLogModal
width={width}
log={log}
onCancel={() => setShowModal(false)}
/>
)
}
</>
)
}
export default Log

View File

@@ -1,22 +1,34 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useRef } from 'react'
import { useContext } from 'use-context-selector'
import s from '../style.module.css'
import type { IChatItem } from '../type'
import Log from '../log'
import MoreInfo from '../more-info'
import AppContext from '@/context/app-context'
import { Markdown } from '@/app/components/base/markdown'
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'>
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'> & {
isShowPromptLog?: boolean
item: IChatItem
isResponsing?: boolean
}
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar }) => {
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => {
const { userProfile } = useContext(AppContext)
const userName = userProfile?.name
const ref = useRef(null)
return (
<div className='flex items-start justify-end' key={id}>
<div className={`flex items-start justify-end ${isShowPromptLog && 'first-of-type:pt-[14px]'}`} key={id} ref={ref}>
<div className={s.questionWrapWrap}>
<div className={`${s.question} relative text-sm text-gray-900`}>
<div className={`${s.question} group relative text-sm text-gray-900`}>
{
isShowPromptLog && !isResponsing && (
<Log log={item.log!} containerRef={ref} />
)
}
<div
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
>

View File

@@ -66,6 +66,7 @@ export type IChatItem = {
annotation?: Annotation
useCurrentUserAvatar?: boolean
isOpeningStatement?: boolean
log?: { role: string; text: string }[]
}
export type MessageEnd = {

View File

@@ -37,7 +37,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
<div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
<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-4 h-4'>{headerIcon}</div>}
{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>
<div>

View File

@@ -4,11 +4,13 @@ import React, { useEffect, useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useBoolean, useClickAway, useGetState } from 'ahooks'
import { Cog8ToothIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
import { InformationCircleIcon } from '@heroicons/react/24/outline'
import produce from 'immer'
import ParamItem from './param-item'
import ModelIcon from './model-icon'
import ModelName from './model-name'
import ModelModeTypeLabel from './model-mode-type-label'
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import Radio from '@/app/components/base/radio'
import Panel from '@/app/components/base/panel'
import type { CompletionParams } from '@/models/debug'
@@ -25,21 +27,23 @@ import Loading from '@/app/components/base/loading'
import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector'
import { ModelType, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import { useProviderContext } from '@/context/provider-context'
import type { ModelModeType } from '@/types/app'
export type IConfigModelProps = {
isAdvancedMode: boolean
mode: string
modelId: string
provider: ProviderEnum
setModelId: (id: string, provider: ProviderEnum) => void
setModel: (model: { id: string; provider: ProviderEnum; mode: ModelModeType }) => void
completionParams: CompletionParams
onCompletionParamsChange: (newParams: CompletionParams) => void
disabled: boolean
}
const ConfigModel: FC<IConfigModelProps> = ({
isAdvancedMode,
modelId,
provider,
setModelId,
setModel,
completionParams,
onCompletionParamsChange,
disabled,
@@ -56,6 +60,8 @@ const ConfigModel: FC<IConfigModelProps> = ({
const hasEnableParams = currParams && Object.keys(currParams).some(key => currParams[key].enabled)
const allSupportParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty', 'max_tokens']
const currSupportParams = currParams ? allSupportParams.filter(key => currParams[key].enabled) : allSupportParams
if (isAdvancedMode)
currSupportParams.push('stop')
useEffect(() => {
(async () => {
@@ -115,11 +121,15 @@ const ConfigModel: FC<IConfigModelProps> = ({
return adjustedValue
}
const handleSelectModel = (id: string, nextProvider = ProviderEnum.openai) => {
const handleSelectModel = ({ id, provider: nextProvider, mode }: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
return async () => {
const prevParamsRule = getAllParams()[provider]?.[modelId]
setModelId(id, nextProvider)
setModel({
id,
provider: nextProvider || ProviderEnum.openai,
mode,
})
await ensureModelParamLoaded(nextProvider, id)
@@ -211,16 +221,26 @@ const ConfigModel: FC<IConfigModelProps> = ({
setToneId(matchToneId(completionParams))
}, [completionParams])
const handleParamChange = (key: string, value: number) => {
const currParamsRule = getAllParams()[provider]?.[modelId]
let notOutRangeValue = parseFloat((value || 0).toFixed(2))
notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue)
notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue)
const handleParamChange = (key: string, value: number | string[]) => {
if (value === undefined)
return
onCompletionParamsChange({
...completionParams,
[key]: notOutRangeValue,
})
if (key === 'stop') {
onCompletionParamsChange({
...completionParams,
[key]: value as string[],
})
}
else {
const currParamsRule = getAllParams()[provider]?.[modelId]
let notOutRangeValue = parseFloat((value as number).toFixed(2))
notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue)
notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue)
onCompletionParamsChange({
...completionParams,
[key]: notOutRangeValue,
})
}
}
const ableStyle = 'bg-indigo-25 border-[#2A87F5] cursor-pointer'
const diabledStyle = 'bg-[#FFFCF5] border-[#F79009]'
@@ -228,7 +248,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
const getToneIcon = (toneId: number) => {
const className = 'w-[14px] h-[14px]'
const res = ({
1: <Brush01 className={className}/>,
1: <Brush01 className={className} />,
2: <Scales02 className={className} />,
3: <Target04 className={className} />,
4: <Sliders02 className={className} />,
@@ -249,17 +269,19 @@ const ConfigModel: FC<IConfigModelProps> = ({
return (
<div className='relative' ref={configContentRef}>
<div
className={cn('flex items-center border h-8 px-2.5 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
className={cn('flex items-center border h-8 px-2 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
onClick={() => !disabled && toogleShowConfig()}
>
<ModelIcon
className='!w-5 !h-5'
modelId={modelId}
providerName={provider}
/>
<div className='text-[13px] text-gray-900 font-medium'>
<ModelName modelId={selectedModel.name} modelDisplayName={currModel?.model_display_name} />
</div>
{disabled ? <InformationCircleIcon className='w-3.5 h-3.5 text-[#F79009]' /> : <Cog8ToothIcon className='w-3.5 h-3.5 text-gray-500' />}
{isAdvancedMode && <ModelModeTypeLabel type={currModel?.model_mode as ModelModeType} isHighlight />}
{disabled ? <InformationCircleIcon className='w-4 h-4 text-[#F79009]' /> : <SlidersH className='w-4 h-4 text-indigo-600' />}
</div>
{isShowConfig && (
<Panel
@@ -282,6 +304,8 @@ const ConfigModel: FC<IConfigModelProps> = ({
<div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div>
<ModelSelector
isShowModelModeType={isAdvancedMode}
isShowAddModel
popClassName='right-0'
triggerIconSmall
value={{
@@ -290,7 +314,11 @@ const ConfigModel: FC<IConfigModelProps> = ({
}}
modelType={ModelType.textGeneration}
onChange={(model) => {
handleSelectModel(model.model_name, model.model_provider.provider_name as ProviderEnum)()
handleSelectModel({
id: model.model_name,
provider: model.model_provider.provider_name as ProviderEnum,
mode: model.model_mode,
})()
}}
/>
</div>
@@ -343,20 +371,21 @@ const ConfigModel: FC<IConfigModelProps> = ({
{/* Params */}
<div className={cn(hasEnableParams && 'mt-4', 'space-y-4', !allParams[provider]?.[modelId] && 'flex items-center min-h-[200px]')}>
{allParams[provider]?.[modelId]
{(allParams[provider]?.[modelId])
? (
currSupportParams.map(key => (<ParamItem
key={key}
id={key}
name={t(`common.model.params.${key}`)}
tip={t(`common.model.params.${key}Tip`)}
name={t(`common.model.params.${key === 'stop' ? 'stop_sequences' : key}`)}
tip={t(`common.model.params.${key === 'stop' ? 'stop_sequences' : key}Tip`)}
{...currParams[key] as any}
value={(completionParams as any)[key] as any}
onChange={handleParamChange}
inputType={key === 'stop' ? 'inputTag' : 'slider'}
/>))
)
: (
<Loading type='area'/>
<Loading type='area' />
)}
</div>
</div>

View File

@@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { ModelModeType } from '@/types/app'
type Props = {
className?: string
type: ModelModeType
isHighlight?: boolean
}
const ModelModeTypeLabel: FC<Props> = ({
className,
type,
isHighlight,
}) => {
const { t } = useTranslation()
return (
<div
className={cn(className, isHighlight ? 'border-indigo-300 text-indigo-600' : 'border-gray-300 text-gray-500', 'flex items-center h-4 px-1 border rounded text-xs font-semibold uppercase')}
>
{t(`appDebug.modelConfig.modeType.${type}`)}
</div>
)
}
export default React.memo(ModelModeTypeLabel)

View File

@@ -1,8 +1,10 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import Slider from '@/app/components/base/slider'
import TagInput from '@/app/components/base/tag-input'
export const getFitPrecisionValue = (num: number, precision: number | null) => {
if (!precision || !(`${num}`).includes('.'))
@@ -19,16 +21,19 @@ export type IParamIteProps = {
id: string
name: string
tip: string
value: number
value: number | string[]
step?: number
min?: number
max: number
precision: number | null
onChange: (key: string, value: number) => void
onChange: (key: string, value: number | string[]) => void
inputType?: 'inputTag' | 'slider'
}
const TIMES_TEMPLATE = '1000000000000'
const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, precision, value, onChange }) => {
const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, precision, value, inputType, onChange }) => {
const { t } = useTranslation()
const getToIntTimes = (num: number) => {
if (precision)
return parseInt(TIMES_TEMPLATE.slice(0, precision + 1), 10)
@@ -45,30 +50,44 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max
}, [value, precision])
return (
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
{/* Give tooltip different tip to avoiding hide bug */}
<Tooltip htmlContent={<div className="w-[200px] whitespace-pre-wrap">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.66667 10.6667H8V8H7.33333M8 5.33333H8.00667M14 8C14 8.78793 13.8448 9.56815 13.5433 10.2961C13.2417 11.0241 12.7998 11.6855 12.2426 12.2426C11.6855 12.7998 11.0241 13.2417 10.2961 13.5433C9.56815 13.8448 8.78793 14 8 14C7.21207 14 6.43185 13.8448 5.7039 13.5433C4.97595 13.2417 4.31451 12.7998 3.75736 12.2426C3.20021 11.6855 2.75825 11.0241 2.45672 10.2961C2.15519 9.56815 2 8.78793 2 8C2 6.4087 2.63214 4.88258 3.75736 3.75736C4.88258 2.63214 6.4087 2 8 2C9.5913 2 11.1174 2.63214 12.2426 3.75736C13.3679 4.88258 14 6.4087 14 8Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Tooltip>
<div className="flex flex-col flex-shrink-0">
<div className="flex items-center">
<span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
{/* Give tooltip different tip to avoiding hide bug */}
<Tooltip htmlContent={<div className="w-[200px] whitespace-pre-wrap">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.66667 10.6667H8V8H7.33333M8 5.33333H8.00667M14 8C14 8.78793 13.8448 9.56815 13.5433 10.2961C13.2417 11.0241 12.7998 11.6855 12.2426 12.2426C11.6855 12.7998 11.0241 13.2417 10.2961 13.5433C9.56815 13.8448 8.78793 14 8 14C7.21207 14 6.43185 13.8448 5.7039 13.5433C4.97595 13.2417 4.31451 12.7998 3.75736 12.2426C3.20021 11.6855 2.75825 11.0241 2.45672 10.2961C2.15519 9.56815 2 8.78793 2 8C2 6.4087 2.63214 4.88258 3.75736 3.75736C4.88258 2.63214 6.4087 2 8 2C9.5913 2 11.1174 2.63214 12.2426 3.75736C13.3679 4.88258 14 6.4087 14 8Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Tooltip>
</div>
{inputType === 'inputTag' && <div className="text-gray-400 text-xs font-normal">{t('common.model.params.stop_sequencesPlaceholder')}</div>}
</div>
<div className="flex items-center">
<div className="mr-4 w-[120px]">
<Slider value={value * times} min={min * times} max={max * times} onChange={(value) => {
onChange(id, value / times)
}} />
</div>
<input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
let value = getFitPrecisionValue(isNaN(parseFloat(e.target.value)) ? min : parseFloat(e.target.value), precision)
if (value < min)
value = min
{inputType === 'inputTag'
? <TagInput
items={(value ?? []) as string[]}
onChange={newSequences => onChange(id, newSequences)}
customizedConfirmKey='Tab'
/>
: (
<>
<div className="mr-4 w-[120px]">
<Slider value={value * times} min={min * times} max={max * times} onChange={(value) => {
onChange(id, value / times)
}} />
</div>
<input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
let value = getFitPrecisionValue(isNaN(parseFloat(e.target.value)) ? min : parseFloat(e.target.value), precision)
if (value < min)
value = min
if (value > max)
value = max
onChange(id, value)
}} />
if (value > max)
value = max
onChange(id, value)
}} />
</>
)
}
</div>
</div>
)

View File

@@ -0,0 +1,185 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import copy from 'copy-to-clipboard'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useBoolean } from 'ahooks'
import produce from 'immer'
import s from './style.module.css'
import MessageTypeSelector from './message-type-selector'
import ConfirmAddVar from './confirm-add-var'
import type { PromptRole, PromptVariable } from '@/models/debug'
import { HelpCircle, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
import { Clipboard, ClipboardCheck } from '@/app/components/base/icons/src/vender/line/files'
import Tooltip from '@/app/components/base/tooltip'
import PromptEditor from '@/app/components/base/prompt-editor'
import ConfigContext from '@/context/debug-configuration'
import { getNewVar, getVars } from '@/utils/var'
import { AppType } from '@/types/app'
type Props = {
type: PromptRole
isChatMode: boolean
value: string
onTypeChange: (value: PromptRole) => void
onChange: (value: string) => void
canDelete: boolean
onDelete: () => void
promptVariables: PromptVariable[]
}
const AdvancedPromptInput: FC<Props> = ({
type,
isChatMode,
value,
onChange,
onTypeChange,
canDelete,
onDelete,
promptVariables,
}) => {
const { t } = useTranslation()
const {
mode,
hasSetBlockStatus,
modelConfig,
setModelConfig,
conversationHistoriesRole,
showHistoryModal,
dataSets,
showSelectDataSet,
} = useContext(ConfigContext)
const isChatApp = mode === AppType.chat
const [isCopied, setIsCopied] = React.useState(false)
const promptVariablesObj = (() => {
const obj: Record<string, boolean> = {}
promptVariables.forEach((item) => {
obj[item.key] = true
})
return obj
})()
const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
const handlePromptChange = (newValue: string) => {
if (value === newValue)
return
onChange(newValue)
}
const handleBlur = () => {
const keys = getVars(value)
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
if (newPromptVariables.length > 0) {
setNewPromptVariables(newPromptVariables)
showConfirmAddVar()
}
}
const handleAutoAdd = (isAdd: boolean) => {
return () => {
if (isAdd) {
const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newPromptVariables]
})
setModelConfig(newModelConfig)
}
hideConfirmAddVar()
}
}
const editorHeight = isChatMode ? 'h-[200px]' : 'h-[508px]'
return (
<div className={`relative ${s.gradientBorder}`}>
<div className='rounded-xl bg-white'>
<div className={cn(s.boxHeader, 'flex justify-between items-center h-11 pt-2 pr-3 pb-1 pl-4 rounded-tl-xl rounded-tr-xl bg-white hover:shadow-xs')}>
{isChatMode
? (
<MessageTypeSelector value={type} onChange={onTypeChange} />
)
: (
<div className='flex items-center space-x-1'>
<div className='text-sm font-semibold uppercase text-indigo-800'>{t('appDebug.pageTitle.line1')}
</div>
<Tooltip
htmlContent={<div className='w-[180px]'>
{t('appDebug.promptTip')}
</div>}
selector='config-prompt-tooltip'>
<HelpCircle className='w-[14px] h-[14px] text-indigo-400' />
</Tooltip>
</div>)}
<div className={cn(s.optionWrap, 'items-center space-x-1')}>
{canDelete && (
<Trash03 onClick={onDelete} className='h-6 w-6 p-1 text-gray-500 cursor-pointer' />
)}
{!isCopied
? (
<Clipboard className='h-6 w-6 p-1 text-gray-500 cursor-pointer' onClick={() => {
copy(value)
setIsCopied(true)
}} />
)
: (
<ClipboardCheck className='h-6 w-6 p-1 text-gray-500' />
)}
</div>
</div>
<div className={cn(editorHeight, 'px-4 min-h-[102px] overflow-y-auto text-sm text-gray-700')}>
<PromptEditor
className={editorHeight}
value={value}
contextBlock={{
selectable: !hasSetBlockStatus.context,
datasets: dataSets.map(item => ({
id: item.id,
name: item.name,
type: item.data_source_type,
})),
onAddContext: showSelectDataSet,
}}
variableBlock={{
variables: modelConfig.configs.prompt_variables.map(item => ({
name: item.name,
value: item.key,
})),
}}
historyBlock={{
show: !isChatMode && isChatApp,
selectable: !hasSetBlockStatus.history,
history: {
user: conversationHistoriesRole?.user_prefix,
assistant: conversationHistoriesRole?.assistant_prefix,
},
onEditRole: showHistoryModal,
}}
queryBlock={{
show: !isChatMode && isChatApp,
selectable: !hasSetBlockStatus.query,
}}
onChange={handlePromptChange}
onBlur={handleBlur}
/>
</div>
<div className='pl-4 pb-2 flex'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value.length}</div>
</div>
</div>
{isShowConfirmAddVar && (
<ConfirmAddVar
varNameArr={newPromptVariables.map(v => v.name)}
onConfrim={handleAutoAdd(true)}
onCancel={handleAutoAdd(false)}
onHide={hideConfirmAddVar}
/>
)}
</div>
)
}
export default React.memo(AdvancedPromptInput)

View File

@@ -1,11 +1,11 @@
'use client'
import React, { FC, useRef } from 'react'
import type { FC } from 'react'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { useClickAway } from 'ahooks'
import VarHighlight from '../../base/var-highlight'
import Button from '@/app/components/base/button'
export interface IConfirmAddVarProps {
export type IConfirmAddVarProps = {
varNameArr: string[]
onConfrim: () => void
onCancel: () => void
@@ -28,19 +28,20 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
}) => {
const { t } = useTranslation()
const mainContentRef = useRef<HTMLDivElement>(null)
useClickAway(() => {
onHide()
}, mainContentRef)
// new prompt editor blur trigger click...
// useClickAway(() => {
// onHide()
// }, mainContentRef)
return (
<div className='absolute inset-0 flex items-center justify-center rounded-xl'
style={{
backgroundColor: 'rgba(35, 56, 118, 0.2)'
backgroundColor: 'rgba(35, 56, 118, 0.2)',
}}>
<div
ref={mainContentRef}
className='w-[420px] rounded-xl bg-gray-50 p-6'
style={{
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
}}
>
<div className='flex items-start space-x-3'>
@@ -48,13 +49,13 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
style={{
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
}}
>{VarIcon}</div>
<div className='grow-1'>
<div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div>
<div className='flex flex-wrap mt-[15px] max-h-[66px] overflow-y-auto px-1 space-x-1'>
{varNameArr.map((name) => (
{varNameArr.map(name => (
<VarHighlight key={name} name={name} />
))}
</div>

View File

@@ -0,0 +1,59 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import type { ConversationHistoriesRole } from '@/models/debug'
import Button from '@/app/components/base/button'
type Props = {
isShow: boolean
saveLoading: boolean
data: ConversationHistoriesRole
onClose: () => void
onSave: (data: any) => void
}
const EditModal: FC<Props> = ({
isShow,
saveLoading,
data,
onClose,
onSave,
}) => {
const { t } = useTranslation()
const [tempData, setTempData] = useState(data)
return (
<Modal
title={t('appDebug.feature.conversationHistory.editModal.title')}
isShow={isShow}
onClose={onClose}
wrapperClassName='!z-[101]'
>
<div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('appDebug.feature.conversationHistory.editModal.userPrefix')}</div>
<input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
value={tempData.user_prefix}
onChange={e => setTempData({
...tempData,
user_prefix: e.target.value,
})}
/>
<div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('appDebug.feature.conversationHistory.editModal.assistantPrefix')}</div>
<input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
value={tempData.assistant_prefix}
onChange={e => setTempData({
...tempData,
assistant_prefix: e.target.value,
})}
placeholder={t('common.chat.conversationNamePlaceholder') || ''}
/>
<div className='mt-10 flex justify-end'>
<Button className='mr-2 flex-shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button type='primary' className='flex-shrink-0' onClick={() => onSave(tempData)} loading={saveLoading}>{t('common.operation.save')}</Button>
</div>
</Modal>
)
}
export default React.memo(EditModal)

View File

@@ -0,0 +1,50 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
type Props = {
showWarning: boolean
onShowEditModal: () => void
}
const HistoryPanel: FC<Props> = ({
showWarning,
onShowEditModal,
}) => {
const { t } = useTranslation()
return (
<Panel
className='mt-3'
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.conversationHistory.title')}</div>
</div>
}
headerIcon={
<div className='p-1 rounded-md bg-white shadow-xs'>
<MessageClockCircle className='w-4 h-4 text-[#DD2590]' />
</div>}
headerRight={
<div className='flex items-center'>
<div className='text-xs text-gray-500'>{t('appDebug.feature.conversationHistory.description')}</div>
<div className='ml-3 w-[1px] h-[14px] bg-gray-200'></div>
<OperationBtn type="edit" onClick={onShowEditModal} />
</div>
}
noBodySpacing
>
{showWarning && (
<div className='flex justify-between py-2 px-3 rounded-b-xl bg-[#FFFAEB] text-xs text-gray-700'>
{/* <div>{t('appDebug.feature.conversationHistory.tip')} <a href="https://docs.dify.ai/getting-started/readme" target='_blank' className='text-[#155EEF]'>{t('appDebug.feature.conversationHistory.learnMore')}</a></div> */}
<div>{t('appDebug.feature.conversationHistory.tip')}</div>
</div>
)}
</Panel>
)
}
export default React.memo(HistoryPanel)

View File

@@ -1,23 +1,23 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import cn from 'classnames'
import ConfirmAddVar from './confirm-add-var'
import s from './style.module.css'
import BlockInput from '@/app/components/base/block-input'
import type { PromptVariable } from '@/models/debug'
import Tooltip from '@/app/components/base/tooltip'
import { AppType } from '@/types/app'
import { getNewVar } from '@/utils/var'
import SimplePromptInput from './simple-prompt-input'
import AdvancedMessageInput from '@/app/components/app/configuration/config-prompt/advanced-prompt-input'
import { PromptRole } from '@/models/debug'
import type { PromptItem, PromptVariable } from '@/models/debug'
import { type AppType, ModelModeType } from '@/types/app'
import ConfigContext from '@/context/debug-configuration'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config'
export type IPromptProps = {
mode: AppType
promptTemplate: string
promptVariables: PromptVariable[]
readonly?: boolean
onChange?: (promp: string, promptVariables: PromptVariable[]) => void
onChange?: (prompt: string, promptVariables: PromptVariable[]) => void
}
const Prompt: FC<IPromptProps> = ({
@@ -28,73 +28,114 @@ const Prompt: FC<IPromptProps> = ({
onChange,
}) => {
const { t } = useTranslation()
const promptVariablesObj = (() => {
const obj: Record<string, boolean> = {}
promptVariables.forEach((item) => {
obj[item.key] = true
const {
isAdvancedMode,
currentAdvancedPrompt,
setCurrentAdvancedPrompt,
modelModeType,
} = useContext(ConfigContext)
const handleMessageTypeChange = (index: number, role: PromptRole) => {
const newPrompt = produce(currentAdvancedPrompt as PromptItem[], (draft) => {
draft[index].role = role
})
return obj
})()
const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
const [newTemplates, setNewTemplates] = React.useState('')
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
const handleChange = (newTemplates: string, keys: string[]) => {
// const hasRemovedKeysInput = promptVariables.filter(input => keys.includes(input.key))
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
if (newPromptVariables.length > 0) {
setNewPromptVariables(newPromptVariables)
setNewTemplates(newTemplates)
showConfirmAddVar()
return
}
onChange?.(newTemplates, [])
setCurrentAdvancedPrompt(newPrompt)
}
const handleAutoAdd = (isAdd: boolean) => {
return () => {
onChange?.(newTemplates, isAdd ? newPromptVariables : [])
hideConfirmAddVar()
const handleValueChange = (value: string, index?: number) => {
if (modelModeType === ModelModeType.chat) {
const newPrompt = produce(currentAdvancedPrompt as PromptItem[], (draft) => {
draft[index as number].text = value
})
setCurrentAdvancedPrompt(newPrompt, true)
}
else {
const prompt = currentAdvancedPrompt as PromptItem
setCurrentAdvancedPrompt({
...prompt,
text: value,
}, true)
}
}
const handleAddMessage = () => {
const currentAdvancedPromptList = currentAdvancedPrompt as PromptItem[]
if (currentAdvancedPromptList.length === 0) {
setCurrentAdvancedPrompt([{
role: PromptRole.system,
text: '',
}])
return
}
const lastMessageType = currentAdvancedPromptList[currentAdvancedPromptList.length - 1].role
const appendMessage = {
role: lastMessageType === PromptRole.user ? PromptRole.assistant : PromptRole.user,
text: '',
}
setCurrentAdvancedPrompt([...currentAdvancedPromptList, appendMessage])
}
const handlePromptDelete = (index: number) => {
const currentAdvancedPromptList = currentAdvancedPrompt as PromptItem[]
const newPrompt = produce(currentAdvancedPromptList, (draft) => {
draft.splice(index, 1)
})
setCurrentAdvancedPrompt(newPrompt)
}
if (!isAdvancedMode) {
return (
<SimplePromptInput
mode={mode}
promptTemplate={promptTemplate}
promptVariables={promptVariables}
readonly={readonly}
onChange={onChange}
/>
)
}
return (
<div className={cn(!readonly ? `${s.gradientBorder}` : 'bg-gray-50', 'relative rounded-xl')}>
<div className="flex items-center h-11 pl-3 gap-1">
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M3.00001 0.100098C3.21218 0.100098 3.41566 0.184383 3.56569 0.334412C3.71572 0.484441 3.80001 0.687924 3.80001 0.900098V1.7001H4.60001C4.81218 1.7001 5.01566 1.78438 5.16569 1.93441C5.31572 2.08444 5.40001 2.28792 5.40001 2.5001C5.40001 2.71227 5.31572 2.91575 5.16569 3.06578C5.01566 3.21581 4.81218 3.3001 4.60001 3.3001H3.80001V4.1001C3.80001 4.31227 3.71572 4.51575 3.56569 4.66578C3.41566 4.81581 3.21218 4.9001 3.00001 4.9001C2.78783 4.9001 2.58435 4.81581 2.43432 4.66578C2.28429 4.51575 2.20001 4.31227 2.20001 4.1001V3.3001H1.40001C1.18783 3.3001 0.98435 3.21581 0.834321 3.06578C0.684292 2.91575 0.600006 2.71227 0.600006 2.5001C0.600006 2.28792 0.684292 2.08444 0.834321 1.93441C0.98435 1.78438 1.18783 1.7001 1.40001 1.7001H2.20001V0.900098C2.20001 0.687924 2.28429 0.484441 2.43432 0.334412C2.58435 0.184383 2.78783 0.100098 3.00001 0.100098ZM3.00001 8.1001C3.21218 8.1001 3.41566 8.18438 3.56569 8.33441C3.71572 8.48444 3.80001 8.68792 3.80001 8.9001V9.7001H4.60001C4.81218 9.7001 5.01566 9.78438 5.16569 9.93441C5.31572 10.0844 5.40001 10.2879 5.40001 10.5001C5.40001 10.7123 5.31572 10.9158 5.16569 11.0658C5.01566 11.2158 4.81218 11.3001 4.60001 11.3001H3.80001V12.1001C3.80001 12.3123 3.71572 12.5158 3.56569 12.6658C3.41566 12.8158 3.21218 12.9001 3.00001 12.9001C2.78783 12.9001 2.58435 12.8158 2.43432 12.6658C2.28429 12.5158 2.20001 12.3123 2.20001 12.1001V11.3001H1.40001C1.18783 11.3001 0.98435 11.2158 0.834321 11.0658C0.684292 10.9158 0.600006 10.7123 0.600006 10.5001C0.600006 10.2879 0.684292 10.0844 0.834321 9.93441C0.98435 9.78438 1.18783 9.7001 1.40001 9.7001H2.20001V8.9001C2.20001 8.68792 2.28429 8.48444 2.43432 8.33441C2.58435 8.18438 2.78783 8.1001 3.00001 8.1001ZM8.60001 0.100098C8.77656 0.100041 8.94817 0.158388 9.0881 0.266047C9.22802 0.373706 9.32841 0.52463 9.37361 0.695298L10.3168 4.2601L13 5.8073C13.1216 5.87751 13.2226 5.9785 13.2928 6.10011C13.363 6.22173 13.4 6.35967 13.4 6.5001C13.4 6.64052 13.363 6.77847 13.2928 6.90008C13.2226 7.02169 13.1216 7.12268 13 7.1929L10.3168 8.7409L9.37281 12.3049C9.32753 12.4754 9.22716 12.6262 9.08732 12.7337C8.94748 12.8413 8.77602 12.8996 8.59961 12.8996C8.42319 12.8996 8.25173 12.8413 8.11189 12.7337C7.97205 12.6262 7.87169 12.4754 7.82641 12.3049L6.88321 8.7401L4.20001 7.1929C4.0784 7.12268 3.97742 7.02169 3.90721 6.90008C3.837 6.77847 3.80004 6.64052 3.80004 6.5001C3.80004 6.35967 3.837 6.22173 3.90721 6.10011C3.97742 5.9785 4.0784 5.87751 4.20001 5.8073L6.88321 4.2593L7.82721 0.695298C7.87237 0.524762 7.97263 0.373937 8.1124 0.266291C8.25216 0.158646 8.42359 0.100217 8.60001 0.100098Z" fill="#5850EC" />
</svg>
<div className="h2">{mode === AppType.chat ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
{!readonly && (
<Tooltip
htmlContent={<div className='w-[180px]'>
{t('appDebug.promptTip')}
</div>}
selector='config-prompt-tooltip'>
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Tooltip>
)}
<div>
<div className='space-y-3'>
{modelModeType === ModelModeType.chat
? (
(currentAdvancedPrompt as PromptItem[]).map((item, index) => (
<AdvancedMessageInput
key={index}
isChatMode
type={item.role as PromptRole}
value={item.text}
onTypeChange={type => handleMessageTypeChange(index, type)}
canDelete={(currentAdvancedPrompt as PromptItem[]).length > 1}
onDelete={() => handlePromptDelete(index)}
onChange={value => handleValueChange(value, index)}
promptVariables={promptVariables}
/>
))
)
: (
<AdvancedMessageInput
type={(currentAdvancedPrompt as PromptItem).role as PromptRole}
isChatMode={false}
value={(currentAdvancedPrompt as PromptItem).text}
onTypeChange={type => handleMessageTypeChange(0, type)}
canDelete={false}
onDelete={() => handlePromptDelete(0)}
onChange={value => handleValueChange(value)}
promptVariables={promptVariables}
/>
)
}
</div>
<BlockInput
readonly={readonly}
value={promptTemplate}
onConfirm={(value: string, vars: string[]) => {
handleChange(value, vars)
}}
/>
{isShowConfirmAddVar && (
<ConfirmAddVar
varNameArr={newPromptVariables.map(v => v.name)}
onConfrim={handleAutoAdd(true)}
onCancel={handleAutoAdd(false)}
onHide={hideConfirmAddVar}
/>
{(modelModeType === ModelModeType.chat && (currentAdvancedPrompt as PromptItem[]).length < MAX_PROMPT_MESSAGE_LENGTH) && (
<div
onClick={handleAddMessage}
className='mt-3 flex items-center h-8 justify-center bg-gray-50 rounded-lg cursor-pointer text-[13px] font-medium text-gray-700 space-x-2'>
<Plus className='w-4 h-4' />
<div>{t('appDebug.promptMode.operation.addMessage')}</div>
</div>
)}
</div>
)

View File

@@ -0,0 +1,50 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useBoolean, useClickAway } from 'ahooks'
import cn from 'classnames'
import { PromptRole } from '@/models/debug'
import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows'
type Props = {
value: PromptRole
onChange: (value: PromptRole) => void
}
const allTypes = [PromptRole.system, PromptRole.user, PromptRole.assistant]
const MessageTypeSelector: FC<Props> = ({
value,
onChange,
}) => {
const [showOption, { setFalse: setHide, toggle: toggleShow }] = useBoolean(false)
const ref = React.useRef(null)
useClickAway(() => {
setHide()
}, ref)
return (
<div className='relative left-[-8px]' ref={ref}>
<div
onClick={toggleShow}
className={cn(showOption && 'bg-indigo-100', 'flex items-center h-7 pl-1.5 pr-1 space-x-0.5 rounded-lg cursor-pointer text-indigo-800')}>
<div className='text-sm font-semibold uppercase'>{value}</div>
<ChevronSelectorVertical className='w-3 h-3 ' />
</div>
{showOption && (
<div className='absolute z-10 top-[30px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white'>
{allTypes.map(type => (
<div
key={type}
onClick={() => {
setHide()
onChange(type)
}}
className='flex items-center h-9 min-w-[44px] px-3 rounded-lg cursor-pointer text-sm font-medium text-gray-700 uppercase hover:bg-gray-50'
>{type}</div>
))
}
</div>
)
}
</div>
)
}
export default React.memo(MessageTypeSelector)

View File

@@ -0,0 +1,174 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import cn from 'classnames'
import produce from 'immer'
import { useContext } from 'use-context-selector'
import ConfirmAddVar from './confirm-add-var'
import s from './style.module.css'
import type { PromptVariable } from '@/models/debug'
import Tooltip from '@/app/components/base/tooltip'
import { AppType } from '@/types/app'
import { getNewVar, getVars } from '@/utils/var'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
import type { AutomaticRes } from '@/service/debug'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import PromptEditor from '@/app/components/base/prompt-editor'
import ConfigContext from '@/context/debug-configuration'
export type ISimplePromptInput = {
mode: AppType
promptTemplate: string
promptVariables: PromptVariable[]
readonly?: boolean
onChange?: (promp: string, promptVariables: PromptVariable[]) => void
}
const Prompt: FC<ISimplePromptInput> = ({
mode,
promptTemplate,
promptVariables,
readonly = false,
onChange,
}) => {
const { t } = useTranslation()
const {
modelConfig,
dataSets,
setModelConfig,
setPrevPromptConfig,
setIntroduction,
hasSetBlockStatus,
showSelectDataSet,
} = useContext(ConfigContext)
const promptVariablesObj = (() => {
const obj: Record<string, boolean> = {}
promptVariables.forEach((item) => {
obj[item.key] = true
})
return obj
})()
const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
const [newTemplates, setNewTemplates] = React.useState('')
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
const handleChange = (newTemplates: string, keys: string[]) => {
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
if (newPromptVariables.length > 0) {
setNewPromptVariables(newPromptVariables)
setNewTemplates(newTemplates)
showConfirmAddVar()
return
}
onChange?.(newTemplates, [])
}
const handleAutoAdd = (isAdd: boolean) => {
return () => {
onChange?.(newTemplates, isAdd ? newPromptVariables : [])
hideConfirmAddVar()
}
}
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = (res: AutomaticRes) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = res.prompt
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
})
setModelConfig(newModelConfig)
setPrevPromptConfig(modelConfig.configs)
if (mode === AppType.chat)
setIntroduction(res.opening_statement)
showAutomaticFalse()
}
return (
<div className={cn(!readonly ? `${s.gradientBorder}` : 'bg-gray-50', ' relative shadow-md')}>
<div className='rounded-xl bg-[#EEF4FF]'>
<div className="flex justify-between items-center h-11 px-3">
<div className="flex items-center space-x-1">
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M3.00001 0.100098C3.21218 0.100098 3.41566 0.184383 3.56569 0.334412C3.71572 0.484441 3.80001 0.687924 3.80001 0.900098V1.7001H4.60001C4.81218 1.7001 5.01566 1.78438 5.16569 1.93441C5.31572 2.08444 5.40001 2.28792 5.40001 2.5001C5.40001 2.71227 5.31572 2.91575 5.16569 3.06578C5.01566 3.21581 4.81218 3.3001 4.60001 3.3001H3.80001V4.1001C3.80001 4.31227 3.71572 4.51575 3.56569 4.66578C3.41566 4.81581 3.21218 4.9001 3.00001 4.9001C2.78783 4.9001 2.58435 4.81581 2.43432 4.66578C2.28429 4.51575 2.20001 4.31227 2.20001 4.1001V3.3001H1.40001C1.18783 3.3001 0.98435 3.21581 0.834321 3.06578C0.684292 2.91575 0.600006 2.71227 0.600006 2.5001C0.600006 2.28792 0.684292 2.08444 0.834321 1.93441C0.98435 1.78438 1.18783 1.7001 1.40001 1.7001H2.20001V0.900098C2.20001 0.687924 2.28429 0.484441 2.43432 0.334412C2.58435 0.184383 2.78783 0.100098 3.00001 0.100098ZM3.00001 8.1001C3.21218 8.1001 3.41566 8.18438 3.56569 8.33441C3.71572 8.48444 3.80001 8.68792 3.80001 8.9001V9.7001H4.60001C4.81218 9.7001 5.01566 9.78438 5.16569 9.93441C5.31572 10.0844 5.40001 10.2879 5.40001 10.5001C5.40001 10.7123 5.31572 10.9158 5.16569 11.0658C5.01566 11.2158 4.81218 11.3001 4.60001 11.3001H3.80001V12.1001C3.80001 12.3123 3.71572 12.5158 3.56569 12.6658C3.41566 12.8158 3.21218 12.9001 3.00001 12.9001C2.78783 12.9001 2.58435 12.8158 2.43432 12.6658C2.28429 12.5158 2.20001 12.3123 2.20001 12.1001V11.3001H1.40001C1.18783 11.3001 0.98435 11.2158 0.834321 11.0658C0.684292 10.9158 0.600006 10.7123 0.600006 10.5001C0.600006 10.2879 0.684292 10.0844 0.834321 9.93441C0.98435 9.78438 1.18783 9.7001 1.40001 9.7001H2.20001V8.9001C2.20001 8.68792 2.28429 8.48444 2.43432 8.33441C2.58435 8.18438 2.78783 8.1001 3.00001 8.1001ZM8.60001 0.100098C8.77656 0.100041 8.94817 0.158388 9.0881 0.266047C9.22802 0.373706 9.32841 0.52463 9.37361 0.695298L10.3168 4.2601L13 5.8073C13.1216 5.87751 13.2226 5.9785 13.2928 6.10011C13.363 6.22173 13.4 6.35967 13.4 6.5001C13.4 6.64052 13.363 6.77847 13.2928 6.90008C13.2226 7.02169 13.1216 7.12268 13 7.1929L10.3168 8.7409L9.37281 12.3049C9.32753 12.4754 9.22716 12.6262 9.08732 12.7337C8.94748 12.8413 8.77602 12.8996 8.59961 12.8996C8.42319 12.8996 8.25173 12.8413 8.11189 12.7337C7.97205 12.6262 7.87169 12.4754 7.82641 12.3049L6.88321 8.7401L4.20001 7.1929C4.0784 7.12268 3.97742 7.02169 3.90721 6.90008C3.837 6.77847 3.80004 6.64052 3.80004 6.5001C3.80004 6.35967 3.837 6.22173 3.90721 6.10011C3.97742 5.9785 4.0784 5.87751 4.20001 5.8073L6.88321 4.2593L7.82721 0.695298C7.87237 0.524762 7.97263 0.373937 8.1124 0.266291C8.25216 0.158646 8.42359 0.100217 8.60001 0.100098Z" fill="#5850EC" />
</svg>
<div className='h2'>{mode === AppType.chat ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
{!readonly && (
<Tooltip
htmlContent={<div className='w-[180px]'>
{t('appDebug.promptTip')}
</div>}
selector='config-prompt-tooltip'>
<HelpCircle className='w-[14px] h-[14px] text-indigo-400' />
</Tooltip>
)}
</div>
<AutomaticBtn onClick={showAutomaticTrue}/>
</div>
<div className='px-4 py-2 min-h-[228px] max-h-[156px] overflow-y-auto bg-white rounded-xl text-sm text-gray-700'>
<PromptEditor
className='min-h-[210px]'
value={promptTemplate}
contextBlock={{
selectable: !hasSetBlockStatus.context,
datasets: dataSets.map(item => ({
id: item.id,
name: item.name,
type: item.data_source_type,
})),
onAddContext: showSelectDataSet,
}}
variableBlock={{
variables: modelConfig.configs.prompt_variables.map(item => ({
name: item.name,
value: item.key,
})),
}}
historyBlock={{
show: false,
selectable: false,
history: {
user: '',
assistant: '',
},
onEditRole: () => {},
}}
queryBlock={{
show: false,
selectable: !hasSetBlockStatus.query,
}}
onChange={(value) => {
handleChange?.(value, [])
}}
onBlur={() => {
handleChange(promptTemplate, getVars(promptTemplate))
}}
/>
</div>
</div>
{isShowConfirmAddVar && (
<ConfirmAddVar
varNameArr={newPromptVariables.map(v => v.name)}
onConfrim={handleAutoAdd(true)}
onCancel={handleAutoAdd(false)}
onHide={hideConfirmAddVar}
/>
)}
{showAutomatic && (
<GetAutomaticResModal
mode={mode as AppType}
isShow={showAutomatic}
onClose={showAutomaticFalse}
onFinished={handleAutomaticRes}
/>
)}
</div>
)
}
export default React.memo(Prompt)

View File

@@ -12,4 +12,12 @@
border-radius: 12px;
padding: 2px;
box-sizing: border-box;
}
.optionWrap {
display: none;
}
.boxHeader:hover .optionWrap {
display: flex;
}

View File

@@ -73,7 +73,6 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
delete newItem.max_length
delete newItem.options
}
console.log(newItem)
return newItem
}
@@ -175,8 +174,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
<Tooltip htmlContent={<div className='w-[180px]'>
{t('appDebug.variableTip')}
</div>} selector='config-var-tooltip'>
<HelpCircle className='w-3.5 h-3.5 text-gray-400'/>
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
</Tooltip>
)}
</div>

View File

@@ -2,7 +2,6 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
export type IAutomaticBtnProps = {
onClick: () => void
@@ -22,12 +21,12 @@ const AutomaticBtn: FC<IAutomaticBtnProps> = ({
const { t } = useTranslation()
return (
<Button className='flex space-x-2 items-center !h-8'
<div className='flex px-3 space-x-2 items-center !h-8 cursor-pointer'
onClick={onClick}
>
{leftIcon}
<span className='text-xs font-semibold text-primary-600 uppercase'>{t('appDebug.operation.automatic')}</span>
</Button>
<span className='text-xs font-semibold text-indigo-600 uppercase'>{t('appDebug.operation.automatic')}</span>
</div>
)
}
export default React.memo(AutomaticBtn)

View File

@@ -1,30 +1,39 @@
'use client'
import React, { FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/solid'
export interface IAddFeatureBtnProps {
export type IAddFeatureBtnProps = {
toBottomHeight: number
onClick: () => void
}
const ITEM_HEIGHT = 48
const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
onClick
toBottomHeight,
onClick,
}) => {
const { t } = useTranslation()
return (
<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
'
className='absolute z-[9] left-0 right-0 flex justify-center pb-4'
style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
top: toBottomHeight - ITEM_HEIGHT,
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)',
}}
onClick={onClick}
>
<PlusIcon className='w-4 h-4 font-semibold' />
<div>{t('appDebug.operation.addFeature')}</div>
<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>
)
}

View File

@@ -1,29 +1,32 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useRef } from 'react'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import { useBoolean, useScroll } from 'ahooks'
import DatasetConfig from '../dataset-config'
import ChatGroup from '../features/chat-group'
import ExperienceEnchanceGroup from '../features/experience-enchance-group'
import Toolbox from '../toolbox'
import HistoryPanel from '../config-prompt/conversation-histroy/history-panel'
import AddFeatureBtn from './feature/add-feature-btn'
import AutomaticBtn from './automatic/automatic-btn'
import type { AutomaticRes } from './automatic/get-automatic-res'
import GetAutomaticResModal from './automatic/get-automatic-res'
import ChooseFeature from './feature/choose-feature'
import useFeature from './feature/use-feature'
import AdvancedModeWaring from '@/app/components/app/configuration/prompt-mode/advanced-mode-waring'
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 { PromptVariable } from '@/models/debug'
import { AppType } from '@/types/app'
import { AppType, ModelModeType } from '@/types/app'
import { useProviderContext } from '@/context/provider-context'
const Config: FC = () => {
const {
mode,
isAdvancedMode,
modelModeType,
canReturnToSimpleMode,
hasSetBlockStatus,
showHistoryModal,
introduction,
setIntroduction,
modelConfig,
@@ -44,6 +47,7 @@ const Config: FC = () => {
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables
// simple mode
const handlePromptChange = (newTemplate: string, newVariables: PromptVariable[]) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = newTemplate
@@ -101,26 +105,29 @@ const Config: FC = () => {
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation)
const hasToolbox = false
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = (res: AutomaticRes) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = res.prompt
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
})
setModelConfig(newModelConfig)
setPrevPromptConfig(modelConfig.configs)
if (mode === AppType.chat)
setIntroduction(res.opening_statement)
showAutomaticFalse()
}
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 className="pb-[20px]">
<div className='flex justify-between items-center mb-4'>
<AddFeatureBtn onClick={showChooseFeatureTrue} />
<AutomaticBtn onClick={showAutomaticTrue}/>
</div>
<div
ref={wrapRef}
className="relative px-6 pb-[50px] overflow-y-auto h-full"
>
<AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
{
(isAdvancedMode && canReturnToSimpleMode) && (
<AdvancedModeWaring />
)
}
{showChooseFeature && (
<ChooseFeature
isShow={showChooseFeature}
@@ -131,14 +138,7 @@ const Config: FC = () => {
showSpeechToTextItem={!!speech2textDefaultModel}
/>
)}
{showAutomatic && (
<GetAutomaticResModal
mode={mode as AppType}
isShow={showAutomatic}
onClose={showAutomaticFalse}
onFinished={handleAutomaticRes}
/>
)}
{/* Template */}
<ConfigPrompt
mode={mode as AppType}
@@ -156,6 +156,14 @@ const Config: FC = () => {
{/* Dataset */}
<DatasetConfig />
{/* Chat History */}
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
<HistoryPanel
showWarning={!hasSetBlockStatus.history}
onShowEditModal={showHistoryModal}
/>
)}
{/* ChatConifig */}
{
hasChatConfig && (

View File

@@ -3,16 +3,13 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useBoolean } from 'ahooks'
import { isEqual } from 'lodash-es'
import produce from 'immer'
import FeaturePanel from '../base/feature-panel'
import OperationBtn from '../base/operation-btn'
import CardItem from './card-item'
import SelectDataSet from './select-dataset'
import ParamsConfig from './params-config'
import ContextVar from './context-var'
import ConfigContext from '@/context/debug-configuration'
import type { DataSet } from '@/models/datasets'
import { AppType } from '@/types/app'
const Icon = (
@@ -31,35 +28,12 @@ const DatasetConfig: FC = () => {
setFormattingChanged,
modelConfig,
setModelConfig,
showSelectDataSet,
} = useContext(ConfigContext)
const selectedIds = dataSet.map(item => item.id)
const hasData = dataSet.length > 0
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
const handleSelect = (data: DataSet[]) => {
if (isEqual(data.map(item => item.id), dataSet.map(item => item.id))) {
hideSelectDataSet()
return
}
setFormattingChanged(true)
if (data.find(item => !item.name)) { // has not loaded selected dataset
const newSelected = produce(data, (draft) => {
data.forEach((item, index) => {
if (!item.name) { // not fetched database
const newItem = dataSet.find(i => i.id === item.id)
if (newItem)
draft[index] = newItem
}
})
})
setDataSet(newSelected)
}
else {
setDataSet(data)
}
hideSelectDataSet()
}
const onRemove = (id: string) => {
setDataSet(dataSet.filter(item => item.id !== id))
setFormattingChanged(true)
@@ -89,7 +63,12 @@ const DatasetConfig: FC = () => {
className='mt-3'
headerIcon={Icon}
title={t('appDebug.feature.dataSet.title')}
headerRight={<OperationBtn type="add" onClick={showSelectDataSet} />}
headerRight={
<div className='flex items-center gap-1'>
<ParamsConfig />
<OperationBtn type="add" onClick={showSelectDataSet} />
</div>
}
hasHeaderBottomBorder={!hasData}
noBodySpacing
>
@@ -120,14 +99,6 @@ const DatasetConfig: FC = () => {
/>
)}
{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={hideSelectDataSet}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}
</FeaturePanel>
)
}

View File

@@ -0,0 +1,181 @@
'use client'
import type { FC } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import { HelpCircle, Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip-plus'
import Slider from '@/app/components/base/slider'
import Switch from '@/app/components/base/switch'
import ConfigContext from '@/context/debug-configuration'
// TODO
const PARAMS_KEY = [
'top_k',
'score_threshold',
]
const PARAMS = {
top_k: {
default: 2,
step: 1,
min: 1,
max: 10,
},
score_threshold: {
default: 0.7,
step: 0.01,
min: 0,
max: 1,
},
} as any
export type IParamItemProps = {
id: string
name: string
tip: string
value: number
enable: boolean
step?: number
min?: number
max: number
onChange: (key: string, value: number) => void
onSwitchChange: (key: string, enable: boolean) => void
}
const ParamItem: FC<IParamItemProps> = ({ id, name, tip, step = 0.1, min = 0, max, value, enable, onChange, onSwitchChange }) => {
return (
<div>
<div className="flex items-center justify-between">
<div className="flex items-center">
{id === 'score_threshold' && (
<Switch
size='md'
defaultValue={enable}
onChange={async (val) => {
onSwitchChange(id, val)
}}
/>
)}
<span className="mx-1 text-gray-800 text-[13px] leading-[18px] font-medium">{name}</span>
<Tooltip popupContent={<div className="w-[200px]">{tip}</div>}>
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
</Tooltip>
</div>
<div className="flex items-center"></div>
</div>
<div className="mt-2 flex items-center justify-between">
<div className="flex items-center h-7">
<div className="w-[148px]">
<Slider
disabled={!enable}
value={max < 5 ? value * 100 : value}
min={min < 1 ? min * 100 : min}
max={max < 5 ? max * 100 : max}
onChange={value => onChange(id, value / (max < 5 ? 100 : 1))}
/>
</div>
</div>
<div className="flex items-center">
<input disabled={!enable} type="number" min={min} max={max} step={step} className="block w-[48px] h-7 text-xs leading-[18px] rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600 disabled:opacity-60" value={value} onChange={(e) => {
const value = parseFloat(e.target.value)
if (value < min || value > max)
return
onChange(id, value)
}} />
</div>
</div>
</div>
)
}
const ParamsConfig: FC = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const {
datasetConfigs,
setDatasetConfigs,
} = useContext(ConfigContext)
const handleParamChange = (key: string, value: number) => {
let notOutRangeValue = parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(PARAMS[key].min, notOutRangeValue)
notOutRangeValue = Math.min(PARAMS[key].max, notOutRangeValue)
if (key === 'top_k') {
setDatasetConfigs({
...datasetConfigs,
top_k: notOutRangeValue,
})
}
else if (key === 'score_threshold') {
setDatasetConfigs({
...datasetConfigs,
[key]: {
enable: datasetConfigs.score_threshold.enable,
value: notOutRangeValue,
},
})
}
}
const handleSwitch = (key: string, enable: boolean) => {
if (key === 'top_k')
return
setDatasetConfigs({
...datasetConfigs,
[key]: {
enable,
value: (datasetConfigs as any)[key].value,
},
})
}
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')}>
<Settings04 className="w-[14px] h-[14px]" />
<div className='text-xs font-medium'>
{t('appDebug.datasetConfig.params')}
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-[240px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
{PARAMS_KEY.map((key: string) => {
const currentValue = key === 'top_k' ? datasetConfigs[key] : (datasetConfigs as any)[key].value
const currentEnableState = key === 'top_k' ? true : (datasetConfigs as any)[key].enable
return (
<ParamItem
key={key}
id={key}
name={t(`appDebug.datasetConfig.${key}`)}
tip={t(`appDebug.datasetConfig.${key}Tip`)}
{...PARAMS[key]}
value={currentValue}
enable={currentEnableState}
onChange={handleParamChange}
onSwitchChange={handleSwitch}
/>
)
})}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ParamsConfig)

View File

@@ -94,6 +94,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
isShow={isShow}
onClose={onClose}
className='w-[400px]'
wrapperClassName='!z-[101]'
title={t('appDebug.feature.dataSet.selectTitle')}
>
{!loaded && (

View File

@@ -11,7 +11,7 @@ import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed'
import GroupName from '../base/group-name'
import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
import { AppType } from '@/types/app'
import { AppType, ModelModeType } from '@/types/app'
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { IChatItem } from '@/app/components/app/chat/type'
import Chat from '@/app/components/app/chat'
@@ -37,6 +37,12 @@ const Debug: FC<IDebug> = ({
const {
appId,
mode,
modelModeType,
hasSetBlockStatus,
isAdvancedMode,
promptMode,
chatPromptConfig,
completionPromptConfig,
introduction,
suggestedQuestionsAfterAnswerConfig,
speechToTextConfig,
@@ -53,6 +59,7 @@ const Debug: FC<IDebug> = ({
modelConfig,
completionParams,
hasSetContextVar,
datasetConfigs,
} = useContext(ConfigContext)
const { speech2textDefaultModel } = useProviderContext()
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
@@ -120,6 +127,18 @@ const Debug: FC<IDebug> = ({
}
const checkCanSend = () => {
if (isAdvancedMode && mode === AppType.chat) {
if (modelModeType === ModelModeType.completion) {
if (!hasSetBlockStatus.history) {
notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
return false
}
if (!hasSetBlockStatus.query) {
notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 })
return false
}
}
}
let hasEmptyInput = ''
const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required }) => {
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
@@ -155,11 +174,15 @@ const Debug: FC<IDebug> = ({
id,
},
}))
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const postModelConfig: BackendModelConfig = {
pre_prompt: modelConfig.configs.prompt_template,
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: {},
completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: '',
dataset_query_variable: contextVar || '',
opening_statement: introduction,
more_like_this: {
enabled: false,
@@ -174,8 +197,15 @@ const Debug: FC<IDebug> = ({
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
dataset_configs: datasetConfigs,
}
if (isAdvancedMode) {
postModelConfig.chat_prompt_config = chatPromptConfig
postModelConfig.completion_prompt_config = completionPromptConfig
}
const data = {
@@ -254,6 +284,11 @@ const Debug: FC<IDebug> = ({
setChatList(produce(getChatList(), (draft) => {
const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) {
const requestion = draft[index - 1]
draft[index - 1] = {
...requestion,
log: newResponseItem.message,
}
draft[index] = {
...draft[index],
more: {
@@ -326,7 +361,10 @@ const Debug: FC<IDebug> = ({
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const postModelConfig: BackendModelConfig = {
pre_prompt: modelConfig.configs.prompt_template,
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: {},
completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
opening_statement: introduction,
@@ -341,8 +379,15 @@ const Debug: FC<IDebug> = ({
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
dataset_configs: datasetConfigs,
}
if (isAdvancedMode) {
postModelConfig.chat_prompt_config = chatPromptConfig
postModelConfig.completion_prompt_config = completionPromptConfig
}
const data = {
@@ -413,6 +458,7 @@ const Debug: FC<IDebug> = ({
isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
isShowCitation={citationConfig.enabled}
isShowCitationHitInfo
isShowPromptLog
/>
</div>
</div>
@@ -427,8 +473,11 @@ const Debug: FC<IDebug> = ({
className="mt-2"
content={completionRes}
isLoading={!completionRes && isResponsing}
isResponsing={isResponsing}
isInstalledApp={false}
messageId={messageId}
isError={false}
onRetry={() => { }}
/>
)}
</div>

View File

@@ -1,9 +1,11 @@
'use client'
import React, { FC } from 'react'
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'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
const SuggestedQuestionsAfterAnswer: FC = () => {
const { t } = useTranslation()
@@ -16,9 +18,7 @@ const SuggestedQuestionsAfterAnswer: FC = () => {
<Tooltip htmlContent={<div className='w-[180px]'>
{t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
</div>} selector='suggestion-question-tooltip'>
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
</Tooltip>
</div>
}

View File

@@ -0,0 +1,176 @@
import { useState } from 'react'
import { clone } from 'lodash-es'
import produce from 'immer'
import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug'
import { PromptMode } from '@/models/debug'
import { AppType, ModelModeType } from '@/types/app'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import { PRE_PROMPT_PLACEHOLDER_TEXT, checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
import { fetchPromptTemplate } from '@/service/debug'
type Param = {
appMode: string
modelModeType: ModelModeType
modelName: string
promptMode: PromptMode
prePrompt: string
onUserChangedPrompt: () => void
hasSetDataSet: boolean
}
const useAdvancedPromptConfig = ({
appMode,
modelModeType,
modelName,
promptMode,
prePrompt,
onUserChangedPrompt,
hasSetDataSet,
}: Param) => {
const isAdvancedPrompt = promptMode === PromptMode.advanced
const [chatPromptConfig, setChatPromptConfig] = useState<ChatPromptConfig>(clone(DEFAULT_CHAT_PROMPT_CONFIG))
const [completionPromptConfig, setCompletionPromptConfig] = useState<CompletionPromptConfig>(clone(DEFAULT_COMPLETION_PROMPT_CONFIG))
const currentAdvancedPrompt = (() => {
if (!isAdvancedPrompt)
return []
return (modelModeType === ModelModeType.chat) ? chatPromptConfig.prompt : completionPromptConfig.prompt
})()
const setCurrentAdvancedPrompt = (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => {
if (!isAdvancedPrompt)
return
if (modelModeType === ModelModeType.chat) {
setChatPromptConfig({
...chatPromptConfig,
prompt: prompt as PromptItem[],
})
}
else {
setCompletionPromptConfig({
...completionPromptConfig,
prompt: prompt as PromptItem,
})
}
if (isUserChanged)
onUserChangedPrompt()
}
const setConversationHistoriesRole = (conversationHistoriesRole: ConversationHistoriesRole) => {
setCompletionPromptConfig({
...completionPromptConfig,
conversation_histories_role: conversationHistoriesRole,
})
}
const hasSetBlockStatus = (() => {
if (!isAdvancedPrompt) {
return {
context: checkHasContextBlock(prePrompt),
history: false,
query: false,
}
}
if (modelModeType === ModelModeType.chat) {
return {
context: !!chatPromptConfig.prompt.find(p => checkHasContextBlock(p.text)),
history: false,
query: !!chatPromptConfig.prompt.find(p => checkHasQueryBlock(p.text)),
}
}
else {
const prompt = completionPromptConfig.prompt.text
return {
context: checkHasContextBlock(prompt),
history: checkHasHistoryBlock(prompt),
query: checkHasQueryBlock(prompt),
}
}
})()
/* prompt: simple to advanced process, or chat model to completion model
* 1. migrate prompt
* 2. change promptMode to advanced
*/
const migrateToDefaultPrompt = async (isMigrateToCompetition?: boolean, toModelModeType?: ModelModeType) => {
const mode = modelModeType
const toReplacePrePrompt = prePrompt || ''
if (!isAdvancedPrompt) {
const { chat_prompt_config, completion_prompt_config } = await fetchPromptTemplate({
appMode,
mode,
modelName,
hasSetDataSet,
})
if (modelModeType === ModelModeType.chat) {
const newPromptConfig = produce(chat_prompt_config, (draft) => {
draft.prompt = draft.prompt.map((p) => {
return {
...p,
text: p.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt),
}
})
})
setChatPromptConfig(newPromptConfig)
}
else {
const newPromptConfig = produce(completion_prompt_config, (draft) => {
draft.prompt.text = draft.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
})
setCompletionPromptConfig(newPromptConfig)
}
return
}
if (isMigrateToCompetition) {
const { completion_prompt_config, chat_prompt_config } = await fetchPromptTemplate({
appMode,
mode: toModelModeType as ModelModeType,
modelName,
hasSetDataSet,
})
if (toModelModeType === ModelModeType.completion) {
const newPromptConfig = produce(completion_prompt_config, (draft) => {
if (!completionPromptConfig.prompt.text)
draft.prompt.text = draft.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
else
draft.prompt.text = completionPromptConfig.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
if (appMode === AppType.chat && completionPromptConfig.conversation_histories_role.assistant_prefix && completionPromptConfig.conversation_histories_role.user_prefix)
draft.conversation_histories_role = completionPromptConfig.conversation_histories_role
})
setCompletionPromptConfig(newPromptConfig)
}
else {
const newPromptConfig = produce(chat_prompt_config, (draft) => {
draft.prompt = draft.prompt.map((p) => {
return {
...p,
text: p.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt),
}
})
})
setChatPromptConfig(newPromptConfig)
}
}
}
return {
chatPromptConfig,
setChatPromptConfig,
completionPromptConfig,
setCompletionPromptConfig,
currentAdvancedPrompt,
setCurrentAdvancedPrompt,
hasSetBlockStatus,
setConversationHistoriesRole,
migrateToDefaultPrompt,
}
}
export default useAdvancedPromptConfig

View File

@@ -7,9 +7,13 @@ import { usePathname } from 'next/navigation'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import cn from 'classnames'
import { clone, isEqual } from 'lodash-es'
import Button from '../../base/button'
import Loading from '../../base/loading'
import type { CompletionParams, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptVariable } from '@/models/debug'
import s from './style.module.css'
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
import type { CompletionParams, DatasetConfigs, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptVariable } from '@/models/debug'
import type { DataSet } from '@/models/datasets'
import type { ModelConfig as BackendModelConfig } from '@/types/app'
import ConfigContext from '@/context/debug-configuration'
@@ -24,7 +28,11 @@ import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from
import { fetchDatasets } from '@/service/datasets'
import AccountSetting from '@/app/components/header/account-setting'
import { useProviderContext } from '@/context/provider-context'
import { AppType } from '@/types/app'
import { AppType, ModelModeType } from '@/types/app'
import { FlipBackward } from '@/app/components/base/icons/src/vender/line/arrows'
import { PromptMode } from '@/models/debug'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
type PublichConfig = {
modelConfig: ModelConfig
@@ -44,6 +52,7 @@ const Configuration: FC = () => {
const [publishedConfig, setPublishedConfig] = useState<PublichConfig | null>(null)
const [conversationId, setConversationId] = useState<string | null>('')
const [introduction, setIntroduction] = useState<string>('')
const [controlClearChatMessage, setControlClearChatMessage] = useState(0)
const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({
@@ -75,6 +84,7 @@ const Configuration: FC = () => {
const [modelConfig, doSetModelConfig] = useState<ModelConfig>({
provider: ProviderEnum.openai,
model_id: 'gpt-3.5-turbo',
mode: ModelModeType.unset,
configs: {
prompt_template: '',
prompt_variables: [] as PromptVariable[],
@@ -87,21 +97,52 @@ const Configuration: FC = () => {
dataSets: [],
})
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>({
top_k: 2,
score_threshold: {
enable: false,
value: 0.7,
},
})
const setModelConfig = (newModelConfig: ModelConfig) => {
doSetModelConfig(newModelConfig)
}
const setModelId = (modelId: string, provider: ProviderEnum) => {
const newModelConfig = produce(modelConfig, (draft: any) => {
draft.provider = provider
draft.model_id = modelId
})
setModelConfig(newModelConfig)
}
const modelModeType = modelConfig.mode
const [dataSets, setDataSets] = useState<DataSet[]>([])
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const hasSetContextVar = !!contextVar
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
const selectedIds = dataSets.map(item => item.id)
const handleSelect = (data: DataSet[]) => {
if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) {
hideSelectDataSet()
return
}
setFormattingChanged(true)
if (data.find(item => !item.name)) { // has not loaded selected dataset
const newSelected = produce(data, (draft) => {
data.forEach((item, index) => {
if (!item.name) { // not fetched database
const newItem = dataSets.find(i => i.id === item.id)
if (newItem)
draft[index] = newItem
}
})
})
setDataSets(newSelected)
}
else {
setDataSets(data)
}
hideSelectDataSet()
}
const [isShowHistoryModal, { setTrue: showHistoryModal, setFalse: hideHistoryModal }] = useBoolean(false)
const syncToPublishedConfig = (_publishedConfig: PublichConfig) => {
const modelConfig = _publishedConfig.modelConfig
setModelConfig(_publishedConfig.modelConfig)
@@ -140,14 +181,101 @@ const Configuration: FC = () => {
return quota_used === quota_limit
})
// Fill old app data missing model mode.
useEffect(() => {
if (hasFetchedDetail && !modelModeType) {
const mode = textGenerationModelList.find(({ model_name }) => model_name === modelConfig.model_id)?.model_mode
if (mode) {
const newModelConfig = produce(modelConfig, (draft) => {
draft.mode = mode
})
setModelConfig(newModelConfig)
}
}
}, [textGenerationModelList, hasFetchedDetail])
const hasSetAPIKEY = hasSetCustomAPIKEY || !isTrailFinished
const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
const [promptMode, doSetPromptMode] = useState(PromptMode.advanced)
const isAdvancedMode = promptMode === PromptMode.advanced
const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true)
const setPromptMode = async (mode: PromptMode) => {
if (mode === PromptMode.advanced) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await migrateToDefaultPrompt()
setCanReturnToSimpleMode(true)
}
doSetPromptMode(mode)
}
const {
chatPromptConfig,
setChatPromptConfig,
completionPromptConfig,
setCompletionPromptConfig,
currentAdvancedPrompt,
setCurrentAdvancedPrompt,
hasSetBlockStatus,
setConversationHistoriesRole,
migrateToDefaultPrompt,
} = useAdvancedPromptConfig({
appMode: mode,
modelName: modelConfig.model_id,
promptMode,
modelModeType,
prePrompt: modelConfig.configs.prompt_template,
hasSetDataSet: dataSets.length > 0,
onUserChangedPrompt: () => {
setCanReturnToSimpleMode(false)
},
})
const setModel = async ({
id: modelId,
provider,
mode: modeMode,
}: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
if (isAdvancedMode) {
const appMode = mode
if (modeMode === ModelModeType.completion) {
if (appMode === AppType.chat) {
if (!completionPromptConfig.prompt.text || !completionPromptConfig.conversation_histories_role.assistant_prefix || !completionPromptConfig.conversation_histories_role.user_prefix)
await migrateToDefaultPrompt(true, ModelModeType.completion)
}
else {
if (!completionPromptConfig.prompt.text)
await migrateToDefaultPrompt(true, ModelModeType.completion)
}
}
if (modeMode === ModelModeType.chat) {
if (chatPromptConfig.prompt.length === 0)
await migrateToDefaultPrompt(true, ModelModeType.chat)
}
}
const newModelConfig = produce(modelConfig, (draft) => {
draft.provider = provider
draft.model_id = modelId
draft.mode = modeMode
})
setModelConfig(newModelConfig)
}
useEffect(() => {
fetchAppDetail({ url: '/apps', id: appId }).then(async (res) => {
setMode(res.mode)
const modelConfig = res.model_config
const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
doSetPromptMode(promptMode)
if (promptMode === PromptMode.advanced) {
setChatPromptConfig(modelConfig.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG) as any)
setCompletionPromptConfig(modelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any)
setCanReturnToSimpleMode(false)
}
const model = res.model_config.model
let datasets: any = null
@@ -177,6 +305,7 @@ const Configuration: FC = () => {
modelConfig: {
provider: model.provider,
model_id: model.name,
mode: model.mode,
configs: {
prompt_template: modelConfig.pre_prompt,
prompt_variables: userInputsFormToPromptVariables(modelConfig.user_input_form, modelConfig.dataset_query_variable),
@@ -197,10 +326,38 @@ const Configuration: FC = () => {
})
}, [appId])
const promptEmpty = mode === AppType.completion && !modelConfig.configs.prompt_template
const promptEmpty = (() => {
if (mode === AppType.chat)
return false
if (isAdvancedMode) {
if (modelModeType === ModelModeType.chat)
return chatPromptConfig.prompt.every(({ text }) => !text)
else
return !completionPromptConfig.prompt.text
}
else { return !modelConfig.configs.prompt_template }
})()
const cannotPublish = (() => {
if (mode === AppType.chat) {
if (!isAdvancedMode)
return false
if (modelModeType === ModelModeType.completion) {
if (!hasSetBlockStatus.history || !hasSetBlockStatus.query)
return true
return false
}
return false
}
else { return promptEmpty }
})()
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
const cannotPublish = promptEmpty || contextVarEmpty
const saveAppConfig = async () => {
const handlePublish = async (isSilence?: boolean) => {
const modelId = modelConfig.model_id
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables
@@ -209,6 +366,18 @@ const Configuration: FC = () => {
notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty'), duration: 3000 })
return
}
if (isAdvancedMode && mode === AppType.chat) {
if (modelModeType === ModelModeType.completion) {
if (!hasSetBlockStatus.history) {
notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
return
}
if (!hasSetBlockStatus.query) {
notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 })
return
}
}
}
if (contextVarEmpty) {
notify({ type: 'error', message: t('appDebug.feature.dataSet.queryVariable.contextVarNotEmpty'), duration: 3000 })
return
@@ -222,7 +391,11 @@ const Configuration: FC = () => {
// new model config data struct
const data: BackendModelConfig = {
pre_prompt: promptTemplate,
// Simple Mode prompt
pre_prompt: !isAdvancedMode ? promptTemplate : '',
prompt_type: promptMode,
chat_prompt_config: {},
completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(promptVariables),
dataset_query_variable: contextVar || '',
opening_statement: introduction || '',
@@ -237,8 +410,15 @@ const Configuration: FC = () => {
model: {
provider: modelConfig.provider,
name: modelId,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
dataset_configs: datasetConfigs,
}
if (isAdvancedMode) {
data.chat_prompt_config = chatPromptConfig
data.completion_prompt_config = completionPromptConfig
}
await updateAppModelConfig({ url: `/apps/${appId}/model-config`, body: data })
@@ -254,7 +434,11 @@ const Configuration: FC = () => {
modelConfig: newModelConfig,
completionParams,
})
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
if (!isSilence)
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
setCanReturnToSimpleMode(false)
return true
}
const [showConfirm, setShowConfirm] = useState(false)
@@ -278,6 +462,20 @@ const Configuration: FC = () => {
hasSetAPIKEY,
isTrailFinished,
mode,
modelModeType,
promptMode,
isAdvancedMode,
setPromptMode,
canReturnToSimpleMode,
setCanReturnToSimpleMode,
chatPromptConfig,
completionPromptConfig,
currentAdvancedPrompt,
setCurrentAdvancedPrompt,
conversationHistoriesRole: completionPromptConfig.conversation_histories_role,
showHistoryModal,
setConversationHistoriesRole,
hasSetBlockStatus,
conversationId,
introduction,
setIntroduction,
@@ -304,23 +502,56 @@ const Configuration: FC = () => {
setCompletionParams,
modelConfig,
setModelConfig,
showSelectDataSet,
dataSets,
setDataSets,
datasetConfigs,
setDatasetConfigs,
hasSetContextVar,
}}
>
<>
<div className="flex flex-col h-full">
<div className='flex items-center justify-between px-6 border-b shrink-0 h-14 boder-gray-100'>
<div className='text-xl text-gray-900'>{t('appDebug.pageTitle')}</div>
<div className='flex items-center justify-between px-6 shrink-0 h-14'>
<div>
<div className='italic text-base font-bold text-gray-900 leading-[18px]'>{t('appDebug.pageTitle.line1')}</div>
<div className='flex items-center h-6 space-x-1 text-xs'>
<div className='text-gray-500 font-medium italic'>{t('appDebug.pageTitle.line2')}</div>
{/* modelModeType missing can not load template */}
{(!isAdvancedMode && modelModeType) && (
<div
onClick={() => setPromptMode(PromptMode.advanced)}
className={'cursor-pointer text-indigo-600'}
>
{t('appDebug.promptMode.simple')}
</div>
)}
{isAdvancedMode && (
<div className='flex items-center space-x-2'>
<div className={`${s.advancedPromptMode} italic text-indigo-600`}>{t('appDebug.promptMode.advanced')}</div>
{canReturnToSimpleMode && (
<div
onClick={() => setPromptMode(PromptMode.simple)}
className='flex items-center h-6 px-2 bg-indigo-600 shadow-xs border border-gray-200 rounded-lg text-white text-xs font-semibold cursor-pointer space-x-1'
>
<FlipBackward className='w-3 h-3 text-white'/>
<div className='text-xs font-semibold uppercase'>{t('appDebug.promptMode.switchBack')}</div>
</div>
)}
</div>
)}
</div>
</div>
<div className='flex items-center'>
{/* Model and Parameters */}
<ConfigModel
isAdvancedMode={isAdvancedMode}
mode={mode}
provider={modelConfig.provider as ProviderEnum}
completionParams={completionParams}
modelId={modelConfig.model_id}
setModelId={setModelId}
setModel={setModel}
onCompletionParamsChange={(newParams: CompletionParams) => {
setCompletionParams(newParams)
}}
@@ -328,14 +559,14 @@ const Configuration: FC = () => {
/>
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
<Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
<Button type='primary' onClick={saveAppConfig} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
<Button type='primary' onClick={() => handlePublish(false)} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
</div>
</div>
<div className='flex grow h-[200px]'>
<div className="w-[574px] shrink-0 h-full overflow-y-auto border-r border-gray-100 py-4 px-6">
<div className="w-1/2 min-w-[560px] shrink-0">
<Config />
</div>
<div className="relative grow h-full overflow-y-auto py-4 px-6 bg-gray-50 flex flex-col">
<div className="relative w-1/2 grow h-full overflow-y-auto py-4 px-6 bg-gray-50 flex flex-col rounded-tl-2xl border-t border-l" style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<Debug hasSetAPIKEY={hasSetAPIKEY} onSetting={showSetAPIKey} />
</div>
</div>
@@ -373,6 +604,28 @@ const Configuration: FC = () => {
{isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
hideSetAPIkey()
}} />}
{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()
}}
/>
)}
</>
</ConfigContext.Provider>
)

View File

@@ -0,0 +1,35 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
const AdvancedModeWarning: FC = () => {
const { t } = useTranslation()
const [show, setShow] = React.useState(true)
if (!show)
return null
return (
<div className='mb-3 py-3 px-4 border border-[#FEF0C7] rounded-xl bg-[#FFFAEB]' >
<div className='mb-2 text-xs leading-[18px] font-bold text-[#DC6803]'>{t('appDebug.promptMode.advancedWarning.title')}</div>
<div className='flex justify-between items-center'>
<div className='text-xs leading-[18px] '>
<span className='text-gray-700'>{t('appDebug.promptMode.advancedWarning.description')}</span>
{/* TODO: Doc link */}
{/* <a
className='font-medium text-[#155EEF]'
href='https://docs.dify.ai/getting-started/readme'
target='_blank'
>
{t('appDebug.promptMode.advancedWarning.learnMore')}
</a> */}
</div>
<div
className='flex items-center h-6 px-2 rounded-md bg-[#fff] border border-gray-200 shadow-xs text-xs font-medium text-primary-600 cursor-pointer'
onClick={() => setShow(false)}
>{t('appDebug.promptMode.advancedWarning.ok')}</div>
</div>
</div>
)
}
export default React.memo(AdvancedModeWarning)

View File

@@ -6,10 +6,9 @@ import { useContext } from 'use-context-selector'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
import { BracketsX as VarIcon } from '@/app/components/base/icons/src/vender/line/development'
import ConfigContext from '@/context/debug-configuration'
import type { PromptVariable } from '@/models/debug'
import { AppType } from '@/types/app'
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'
@@ -21,23 +20,13 @@ export type IPromptValuePanelProps = {
onSend?: () => void
}
const starIcon = (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.75 1C2.75 0.723858 2.52614 0.5 2.25 0.5C1.97386 0.5 1.75 0.723858 1.75 1V1.75H1C0.723858 1.75 0.5 1.97386 0.5 2.25C0.5 2.52614 0.723858 2.75 1 2.75H1.75V3.5C1.75 3.77614 1.97386 4 2.25 4C2.52614 4 2.75 3.77614 2.75 3.5V2.75H3.5C3.77614 2.75 4 2.52614 4 2.25C4 1.97386 3.77614 1.75 3.5 1.75H2.75V1Z" fill="#444CE7" />
<path d="M2.75 8.5C2.75 8.22386 2.52614 8 2.25 8C1.97386 8 1.75 8.22386 1.75 8.5V9.25H1C0.723858 9.25 0.5 9.47386 0.5 9.75C0.5 10.0261 0.723858 10.25 1 10.25H1.75V11C1.75 11.2761 1.97386 11.5 2.25 11.5C2.52614 11.5 2.75 11.2761 2.75 11V10.25H3.5C3.77614 10.25 4 10.0261 4 9.75C4 9.47386 3.77614 9.25 3.5 9.25H2.75V8.5Z" fill="#444CE7" />
<path d="M6.96667 1.32051C6.8924 1.12741 6.70689 1 6.5 1C6.29311 1 6.10759 1.12741 6.03333 1.32051L5.16624 3.57494C5.01604 3.96546 4.96884 4.078 4.90428 4.1688C4.8395 4.2599 4.7599 4.3395 4.6688 4.40428C4.578 4.46884 4.46546 4.51604 4.07494 4.66624L1.82051 5.53333C1.62741 5.60759 1.5 5.79311 1.5 6C1.5 6.20689 1.62741 6.39241 1.82051 6.46667L4.07494 7.33376C4.46546 7.48396 4.578 7.53116 4.6688 7.59572C4.7599 7.6605 4.8395 7.7401 4.90428 7.8312C4.96884 7.922 5.01604 8.03454 5.16624 8.42506L6.03333 10.6795C6.1076 10.8726 6.29311 11 6.5 11C6.70689 11 6.89241 10.8726 6.96667 10.6795L7.83376 8.42506C7.98396 8.03454 8.03116 7.922 8.09572 7.8312C8.1605 7.7401 8.2401 7.6605 8.3312 7.59572C8.422 7.53116 8.53454 7.48396 8.92506 7.33376L11.1795 6.46667C11.3726 6.39241 11.5 6.20689 11.5 6C11.5 5.79311 11.3726 5.60759 11.1795 5.53333L8.92506 4.66624C8.53454 4.51604 8.422 4.46884 8.3312 4.40428C8.2401 4.3395 8.1605 4.2599 8.09572 4.1688C8.03116 4.078 7.98396 3.96546 7.83376 3.57494L6.96667 1.32051Z" fill="#444CE7" />
</svg>
)
const PromptValuePanel: FC<IPromptValuePanelProps> = ({
appType,
onSend,
}) => {
const { t } = useTranslation()
const { modelConfig, inputs, setInputs, mode } = useContext(ConfigContext)
const [promptPreviewCollapse, setPromptPreviewCollapse] = useState(false)
const { modelModeType, modelConfig, inputs, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
return key && key?.trim() && name && name?.trim()
})
@@ -50,7 +39,18 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
return obj
})()
const canNotRun = mode === AppType.completion && !modelConfig.configs.prompt_template
const canNotRun = (() => {
if (mode !== AppType.completion)
return true
if (isAdvancedMode) {
if (modelModeType === ModelModeType.chat)
return chatPromptConfig.prompt.every(({ text }) => !text)
return !completionPromptConfig.prompt.text
}
else { return !modelConfig.configs.prompt_template }
})()
const renderRunButton = () => {
return (
<Button
@@ -83,61 +83,21 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
setInputs(newInputs)
}
const promptPreview = (
<div className='py-3 rounded-t-xl bg-indigo-25'>
<div className="px-4">
<div className="flex items-center space-x-1 cursor-pointer" onClick={() => setPromptPreviewCollapse(!promptPreviewCollapse)}>
{starIcon}
<div className="text-xs font-medium text-indigo-600 uppercase">{t('appDebug.inputs.previewTitle')}</div>
{
promptPreviewCollapse
? <ChevronRight className='w-3 h-3 text-gray-700' />
: <ChevronDown className='w-3 h-3 text-gray-700' />
}
</div>
{
!promptPreviewCollapse && (
<div className='mt-2 leading-normal'>
{
(promptTemplate && promptTemplate?.trim())
? (
<div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}}
>
</div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
)
}
</div>
)
}
</div>
</div>
)
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)',
}}>
{promptPreview}
<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)}>
<div className='flex items-center justify-center w-4 h-4'><VarIcon className='w-4 h-4 text-primary-500'/></div>
<div className='text-xs font-medium text-gray-800'>{t('appDebug.inputs.userInputField')}</div>
{
userInputFieldCollapse
? <ChevronRight className='w-3 h-3 text-gray-700' />
: <ChevronDown className='w-3 h-3 text-gray-700' />
? <ChevronRight className='w-3 h-3 text-gray-300' />
: <ChevronDown className='w-3 h-3 text-gray-300' />
}
<div className='text-xs font-medium text-gray-800 uppercase'>{t('appDebug.inputs.userInputField')}</div>
</div>
{appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
<div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
@@ -150,8 +110,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
? (
<div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="flex justify-between">
<div className="mr-1 pt-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
<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'

View File

@@ -0,0 +1,14 @@
.advancedPromptMode {
position: relative;
}
.advancedPromptMode::before {
content: '';
position: absolute;
bottom: 0;
left: -1px;
width: 100%;
height: 3px;
background-color: rgba(68, 76, 231, 0.18);
transform: skewX(-30deg);
}

View File

@@ -9,14 +9,14 @@ import {
InformationCircleIcon,
XMarkIcon,
} from '@heroicons/react/24/outline'
import { SparklesIcon } from '@heroicons/react/24/solid'
import { get } from 'lodash-es'
import InfiniteScroll from 'react-infinite-scroll-component'
import dayjs from 'dayjs'
import { createContext, useContext } from 'use-context-selector'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
import VarPanel from './var-panel'
import { randomString } from '@/utils'
import { EditIconSolid } from '@/app/components/app/chat/icon-component'
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
@@ -32,6 +32,8 @@ import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversa
import { TONE_LIST } from '@/config'
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
import ModelName from '@/app/components/app/configuration/config-model/model-name'
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
import { ModelModeType } from '@/types/app'
type IConversationList = {
logs?: ChatConversationsResponse | CompletionConversationsResponse
@@ -78,6 +80,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
log: item.message as any,
})
newChatList.push({
@@ -102,7 +105,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
type IDetailPanel<T> = {
detail: T
detail: any
onFeedback: FeedbackFunc
onSubmitAnnotation: SubmitAnnotationFunc
}
@@ -112,7 +115,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
const { t } = useTranslation()
const [items, setItems] = React.useState<IChatItem[]>([])
const [hasMore, setHasMore] = useState(true)
const [varValues, setVarValues] = useState<Record<string, string>>({})
const fetchData = async () => {
try {
if (!hasMore)
@@ -128,6 +131,10 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
url: `/apps/${appDetail?.id}/chat-messages`,
params,
})
if (messageRes.data.length > 0) {
const varValues = messageRes.data[0].inputs
setVarValues(varValues)
}
const newItems = [...getFormattedChatList(messageRes.data), ...items]
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
newItems.unshift({
@@ -153,7 +160,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
const isChatMode = appDetail?.mode === 'chat'
const targetTone = TONE_LIST.find((item) => {
const targetTone = TONE_LIST.find((item: any) => {
let res = true
validatedParams.forEach((param) => {
res = item.config?.[param] === detail.model_config?.configs?.completion_params?.[param]
@@ -161,53 +168,81 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
return res
})?.name ?? 'custom'
const modelName = (detail.model_config as any).model.name
const provideName = (detail.model_config as any).model.provider as any
const varList = (detail.model_config as any).user_input_form.map((item: any) => {
const itemContent = item[Object.keys(item)[0]]
return {
label: itemContent.variable,
value: varValues[itemContent.variable],
}
})
const getParamValue = (param: string) => {
const value = detail?.model_config.model?.completion_params?.[param] || '-'
if (param === 'stop') {
if (!value || value.length === 0)
return '-'
return value.join(',')
}
return value
}
return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
{/* Panel Header */}
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
<div className='flex-1'>
<span className='text-gray-500 text-[10px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</span>
<div className='text-gray-800 text-sm'>{isChatMode ? detail.id : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
<div>
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
<div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
</div>
<div className='mr-2 bg-gray-50 py-1.5 px-2.5 rounded-lg flex items-center text-[13px]'>
<ModelIcon
className={classNames('mr-1.5', 'w-5 h-5')}
modelId={detail.model_config.model.name}
providerName={detail.model_config.model.provider}
/>
<ModelName modelId={detail.model_config.model.name} modelDisplayName={detail.model_config.model.name} />
</div>
<Popover
position='br'
className='!w-[280px]'
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
btnElement={<>
<span className='text-[13px]'>{targetTone}</span>
<InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
</>}
htmlContent={<div className='w-[280px]'>
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
<span>Tone of responses</span>
<div>{targetTone}</div>
<div className='flex items-center'>
<div
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
>
<ModelIcon
className='!w-5 !h-5'
modelId={modelName}
providerName={provideName}
/>
<div className='text-[13px] text-gray-900 font-medium'>
<ModelName modelId={modelName} modelDisplayName={modelName} />
</div>
{['temperature', 'top_p', 'presence_penalty', 'max_tokens'].map((param: string, index: number) => {
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
<span className='text-gray-800 font-medium text-xs'>{detail?.model_config.model?.completion_params?.[param] || '-'}</span>
<ModelModeTypeLabel type={ModelModeType.chat} isHighlight />
</div>
<Popover
position='br'
className='!w-[280px]'
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
btnElement={<>
<span className='text-[13px]'>{targetTone}</span>
<InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
</>}
htmlContent={<div className='w-[280px]'>
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
<span>Tone of responses</span>
<div>{targetTone}</div>
</div>
})}
</div>}
/>
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
</div>
})}
</div>}
/>
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
</div>
</div>
</div>
{/* Panel Body */}
<div className='bg-gray-50 border border-gray-100 px-4 py-3 mx-6 my-4 rounded-lg'>
<div className='text-gray-500 text-xs flex items-center'>
<SparklesIcon className='h-3 w-3 mr-1' />{isChatMode ? t('appLog.detail.promptTemplateBeforeChat') : t('appLog.detail.promptTemplate')}
{varList.length > 0 && (
<div className='px-6 pt-4 pb-2'>
<VarPanel varList={varList} />
</div>
<div className='text-gray-700 font-medium text-sm mt-2'>{detail.model_config?.pre_prompt || emptyText}</div>
</div>
)}
{!isChatMode
? <div className="px-2.5 py-4">
<Chat
@@ -216,6 +251,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
onFeedback={onFeedback}
onSubmitAnnotation={onSubmitAnnotation}
displayScene='console'
isShowPromptLog
/>
</div>
: items.length < 8
@@ -226,6 +262,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
onFeedback={onFeedback}
onSubmitAnnotation={onSubmitAnnotation}
displayScene='console'
isShowPromptLog
/>
</div>
: <div
@@ -265,6 +302,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
onFeedback={onFeedback}
onSubmitAnnotation={onSubmitAnnotation}
displayScene='console'
isShowPromptLog
/>
</InfiniteScroll>
</div>
@@ -382,7 +420,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
className={(isHighlight && !isChatMode) ? '' : '!hidden'}
selector={`highlight-${randomString(16)}`}
>
<div className={classNames(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
<div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
{value || '-'}
</div>
</Tooltip>
@@ -413,9 +451,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
</tr>
</thead>
<tbody className="text-gray-500">
{logs.data.map((log) => {
{logs.data.map((log: any) => {
const endUser = log.from_end_user_session_id
const leftValue = get(log, isChatMode ? 'summary' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
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}

View File

@@ -0,0 +1,48 @@
'use client'
import { useBoolean } from 'ahooks'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronDown, ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
type Props = {
varList: { label: string; value: string }[]
}
const VarPanel: FC<Props> = ({
varList,
}) => {
const { t } = useTranslation()
const [isCollapse, { toggle: toggleCollapse }] = useBoolean(false)
return (
<div className='rounded-xl border border-color-indigo-100 bg-indigo-25'>
<div
className='flex items-center h-6 pl-2 py-6 space-x-1 cursor-pointer'
onClick={toggleCollapse}
>
{
isCollapse
? <ChevronRight className='w-3 h-3 text-gray-300' />
: <ChevronDown className='w-3 h-3 text-gray-300' />
}
<div className='text-sm font-semibold text-indigo-800 uppercase'>{t('appLog.detail.variables')}</div>
</div>
{!isCollapse && (
<div className='px-6 pb-3'>
{varList.map(({ label, value }, index) => (
<div key={index} className='flex py-1 leading-[18px] text-[13px]'>
<div className='shrink-0 w-[128px] flex text-primary-600'>
<span className='shrink-0 opacity-60'>{'{{'}</span>
<span className='truncate'>{label}</span>
<span className='shrink-0 opacity-60'>{'}}'}</span>
</div>
<div className='pl-2.5 break-all'>{value}</div>
</div>
))}
</div>
)}
</div>
)
}
export default React.memo(VarPanel)

View File

@@ -165,7 +165,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
console.log(icon, icon_background)
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}

View File

@@ -1,21 +1,24 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import type { Dispatch, FC, SetStateAction } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import copy from 'copy-to-clipboard'
import { useParams } from 'next/navigation'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks'
import { HashtagIcon } from '@heroicons/react/24/solid'
import PromptLog from '@/app/components/app/chat/log'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import type { Feedbacktype } from '@/app/components/app/chat/type'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { Clipboard } from '@/app/components/base/icons/src/vender/line/files'
import { Clipboard, 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 { fetchTextGenerationMessge } from '@/service/debug'
const MAX_DEPTH = 3
export type IGenerationItemProps = {
className?: string
@@ -23,7 +26,9 @@ export type IGenerationItemProps = {
onRetry: () => void
content: string
messageId?: string | null
conversationId?: string
isLoading?: boolean
isResponsing?: boolean
isInWebApp?: boolean
moreLikeThis?: boolean
depth?: number
@@ -64,6 +69,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
content,
messageId,
isLoading,
isResponsing,
moreLikeThis,
isInWebApp = false,
feedback,
@@ -77,14 +83,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
controlClearMoreLikeThis,
}) => {
const { t } = useTranslation()
const params = useParams()
const isTop = depth === 1
const ref = useRef(null)
const [completionRes, setCompletionRes] = useState('')
const [childMessageId, setChildMessageId] = useState<string | null>(null)
const hasChild = !!childMessageId
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
rating: null,
})
const [promptLog, setPromptLog] = useState<{ role: string; text: string }[]>([])
const handleFeedback = async (childFeedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
@@ -150,8 +158,17 @@ const GenerationItem: FC<IGenerationItemProps> = ({
setChildMessageId(null)
}, [isLoading])
const handleOpenLogModal = async (setModal: Dispatch<SetStateAction<boolean>>) => {
const data = await fetchTextGenerationMessge({
appId: params.appId,
messageId: messageId!,
})
setPromptLog(data.message as any || [])
setModal(true)
}
return (
<div className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
<div ref={ref} className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
style={isTop
? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
@@ -186,6 +203,26 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
{
!isInWebApp && !isInstalledApp && !isResponsing && (
<PromptLog
log={promptLog}
containerRef={ref}
>
{
showModal => (
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-2')}
onClick={() => handleOpenLogModal(showModal)}>
<File02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.log')}</div>}
</SimpleBtn>
)
}
</PromptLog>
)
}
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1')}