completion debug & preview

This commit is contained in:
JzoNg 2024-09-01 11:24:54 +08:00
parent 65a6265ff6
commit 8c785e268b
9 changed files with 206 additions and 197 deletions

View File

@ -8,6 +8,7 @@ import { useBoolean } from 'ahooks'
import { import {
RiAddLine, RiAddLine,
RiEqualizer2Line, RiEqualizer2Line,
RiSparklingFill,
} from '@remixicon/react' } from '@remixicon/react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
@ -47,6 +48,7 @@ import { useProviderContext } from '@/context/provider-context'
import AgentLogModal from '@/app/components/base/agent-log-modal' import AgentLogModal from '@/app/components/base/agent-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks'
type IDebug = { type IDebug = {
isAPIKeySet: boolean isAPIKeySet: boolean
@ -82,8 +84,8 @@ const Debug: FC<IDebug> = ({
speechToTextConfig, speechToTextConfig,
textToSpeechConfig, textToSpeechConfig,
citationConfig, citationConfig,
moderationConfig, // moderationConfig,
moreLikeThisConfig, // moreLikeThisConfig,
formattingChanged, formattingChanged,
setFormattingChanged, setFormattingChanged,
dataSets, dataSets,
@ -200,6 +202,7 @@ const Debug: FC<IDebug> = ({
const [completionRes, setCompletionRes] = useState('') const [completionRes, setCompletionRes] = useState('')
const [messageId, setMessageId] = useState<string | null>(null) const [messageId, setMessageId] = useState<string | null>(null)
const features = useFeatures(s => s.features)
const sendTextCompletion = async () => { const sendTextCompletion = async () => {
if (isResponding) { if (isResponding) {
@ -230,36 +233,33 @@ const Debug: FC<IDebug> = ({
completion_prompt_config: {}, completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables), user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '', dataset_query_variable: contextVar || '',
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
more_like_this: moreLikeThisConfig,
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
text_to_speech: {
enabled: false,
voice: '',
language: '',
},
agent_mode: {
enabled: false,
tools: [],
},
dataset_configs: { dataset_configs: {
...datasetConfigs, ...datasetConfigs,
datasets: { datasets: {
datasets: [...postDatasets], datasets: [...postDatasets],
} as any, } as any,
}, },
agent_mode: {
enabled: false,
tools: [],
},
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
more_like_this: features.moreLikeThis as any,
sensitive_word_avoidance: features.moderation as any,
text_to_speech: features.text2speech as any,
// ##TODO## file_upload
file_upload: { file_upload: {
image: visionConfig, image: visionConfig,
}, },
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
} }
if (isAdvancedMode) { if (isAdvancedMode) {
@ -449,7 +449,6 @@ const Debug: FC<IDebug> = ({
<ChatUserInput inputs={inputs} /> <ChatUserInput inputs={inputs} />
</div> </div>
)} )}
{/* ##TODO## new style of completion */}
{mode === AppType.completion && ( {mode === AppType.completion && (
<PromptValuePanel <PromptValuePanel
appType={mode as AppType} appType={mode as AppType}
@ -509,26 +508,36 @@ const Debug: FC<IDebug> = ({
)} )}
{/* Text Generation */} {/* Text Generation */}
{mode === AppType.completion && ( {mode === AppType.completion && (
<div className="mt-6 px-3 pb-4"> <>
<GroupName name={t('appDebug.result')} />
{(completionRes || isResponding) && ( {(completionRes || isResponding) && (
<TextGeneration <>
className="mt-2" <div className='mx-4 mt-3'><GroupName name={t('appDebug.result')} /></div>
content={completionRes} <div className='mx-3 mb-8'>
isLoading={!completionRes && isResponding} <TextGeneration
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel} className="mt-2"
isResponding={isResponding} content={completionRes}
isInstalledApp={false} isLoading={!completionRes && isResponding}
messageId={messageId} isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isError={false} isResponding={isResponding}
onRetry={() => { }} isInstalledApp={false}
supportAnnotation messageId={messageId}
appId={appId} isError={false}
varList={varList} onRetry={() => { }}
siteInfo={null} supportAnnotation
/> appId={appId}
varList={varList}
siteInfo={null}
/>
</div>
</>
)} )}
</div> {!completionRes && !isResponding && (
<div className='grow flex flex-col items-center justify-center gap-2'>
<RiSparklingFill className='w-12 h-12 text-text-empty-state-icon' />
<div className='text-text-quaternary system-sm-regular'>{t('appDebug.noResult')}</div>
</div>
)}
</>
)} )}
{mode === AppType.completion && showPromptLogModal && ( {mode === AppType.completion && showPromptLogModal && (
<PromptLogModal <PromptLogModal

View File

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

View File

@ -0,0 +1,13 @@
import type { PromptVariable } from '@/models/debug'
export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return name
}
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return valueObj ? `{{${valueObj.name}}}` : match
})
}

View File

@ -18,7 +18,7 @@ import type {
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { ssePost } from '@/service/base' import { ssePost } from '@/service/base'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils'
import type { Annotation } from '@/models/log' import type { Annotation } from '@/models/log'
import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'

View File

@ -9,11 +9,13 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
isChatMode?: boolean
disabled?: boolean disabled?: boolean
onFeatureBarClick?: (state: boolean) => void onFeatureBarClick?: (state: boolean) => void
} }
const FeatureBar = ({ const FeatureBar = ({
isChatMode = true,
disabled, disabled,
onFeatureBarClick, onFeatureBarClick,
}: Props) => { }: Props) => {
@ -22,8 +24,13 @@ const FeatureBar = ({
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
const noFeatureEnabled = useMemo(() => { const noFeatureEnabled = useMemo(() => {
return !Object.values(features).some(f => f.enabled) // completion app citation is always true but not enabled for setting
}, [features]) const data = {
...features,
citation: { enabled: isChatMode ? features.citation?.enabled : false },
}
return !Object.values(data).some(f => f.enabled)
}, [features, isChatMode])
return ( return (
<div className='-translate-y-2 m-1 mt-0 px-2.5 py-2 pt-4 bg-util-colors-indigo-indigo-50 rounded-b-[10px] border-l border-b border-r border-components-panel-border-subtle'> <div className='-translate-y-2 m-1 mt-0 px-2.5 py-2 pt-4 bg-util-colors-indigo-indigo-50 rounded-b-[10px] border-l border-b border-r border-components-panel-border-subtle'>
@ -102,7 +109,7 @@ const FeatureBar = ({
</div> </div>
</Tooltip> </Tooltip>
)} )}
{!!features.citation?.enabled && ( {isChatMode && !!features.citation?.enabled && (
<Tooltip <Tooltip
popupContent={t('appDebug.feature.citation.title')} popupContent={t('appDebug.feature.citation.title')}
> >

View File

@ -16,7 +16,7 @@ import type {
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import type { VisionFile } from '@/types/app' import type { VisionFile } from '@/types/app'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils'
type GetAbortController = (abortController: AbortController) => void type GetAbortController = (abortController: AbortController) => void
type SendCallback = { type SendCallback = {

View File

@ -437,6 +437,7 @@ const translation = {
run: 'RUN', run: 'RUN',
}, },
result: 'Output Text', result: 'Output Text',
noResult: 'Output will be displayed here.',
datasetConfig: { datasetConfig: {
settingTitle: 'Retrieval settings', settingTitle: 'Retrieval settings',
knowledgeTip: 'Click the “+” button to add knowledge', knowledgeTip: 'Click the “+” button to add knowledge',

View File

@ -430,6 +430,7 @@ const translation = {
run: '运行', run: '运行',
}, },
result: '结果', result: '结果',
noResult: '输出结果展示在这',
datasetConfig: { datasetConfig: {
settingTitle: '召回设置', settingTitle: '召回设置',
knowledgeTip: '点击 “+” 按钮添加知识库', knowledgeTip: '点击 “+” 按钮添加知识库',

View File

@ -212,7 +212,7 @@ export type ModelConfig = {
user_input_form: UserInputFormItem[] user_input_form: UserInputFormItem[]
dataset_query_variable?: string dataset_query_variable?: string
more_like_this: { more_like_this: {
enabled: boolean enabled?: boolean
} }
suggested_questions_after_answer: { suggested_questions_after_answer: {
enabled: boolean enabled: boolean