Unverified Commit 5b9858a8 authored by zxhlyh's avatar zxhlyh Committed by GitHub

feat: advanced prompt (#1330)

Co-authored-by: 's avatarJoel <iamjoel007@gmail.com>
Co-authored-by: 's avatarJzoNg <jzongcode@gmail.com>
Co-authored-by: 's avatarGillian97 <jinling.sunshine@gmail.com>
parent 42a5b3ec
...@@ -53,6 +53,7 @@ export type IChatProps = { ...@@ -53,6 +53,7 @@ export type IChatProps = {
isShowConfigElem?: boolean isShowConfigElem?: boolean
dataSets?: DataSet[] dataSets?: DataSet[]
isShowCitationHitInfo?: boolean isShowCitationHitInfo?: boolean
isShowPromptLog?: boolean
} }
const Chat: FC<IChatProps> = ({ const Chat: FC<IChatProps> = ({
...@@ -81,6 +82,7 @@ const Chat: FC<IChatProps> = ({ ...@@ -81,6 +82,7 @@ const Chat: FC<IChatProps> = ({
isShowConfigElem, isShowConfigElem,
dataSets, dataSets,
isShowCitationHitInfo, isShowCitationHitInfo,
isShowPromptLog,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
...@@ -186,7 +188,18 @@ const Chat: FC<IChatProps> = ({ ...@@ -186,7 +188,18 @@ const Chat: FC<IChatProps> = ({
isShowCitationHitInfo={isShowCitationHitInfo} 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> </div>
{ {
......
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
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useRef } from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import s from '../style.module.css' import s from '../style.module.css'
import type { IChatItem } from '../type' import type { IChatItem } from '../type'
import Log from '../log'
import MoreInfo from '../more-info' import MoreInfo from '../more-info'
import AppContext from '@/context/app-context' import AppContext from '@/context/app-context'
import { Markdown } from '@/app/components/base/markdown' 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 { userProfile } = useContext(AppContext)
const userName = userProfile?.name const userName = userProfile?.name
const ref = useRef(null)
return ( 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.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 <div
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'} className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
> >
......
...@@ -66,6 +66,7 @@ export type IChatItem = { ...@@ -66,6 +66,7 @@ export type IChatItem = {
annotation?: Annotation annotation?: Annotation
useCurrentUserAvatar?: boolean useCurrentUserAvatar?: boolean
isOpeningStatement?: boolean isOpeningStatement?: boolean
log?: { role: string; text: string }[]
} }
export type MessageEnd = { export type MessageEnd = {
......
...@@ -37,7 +37,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({ ...@@ -37,7 +37,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
<div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}> <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 justify-between items-center h-8'>
<div className='flex items-center space-x-1 shrink-0'> <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 className='text-sm font-semibold text-gray-800'>{title}</div>
</div> </div>
<div> <div>
......
...@@ -4,11 +4,13 @@ import React, { useEffect, useState } from 'react' ...@@ -4,11 +4,13 @@ import React, { useEffect, useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean, useClickAway, useGetState } from 'ahooks' 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 produce from 'immer'
import ParamItem from './param-item' import ParamItem from './param-item'
import ModelIcon from './model-icon' import ModelIcon from './model-icon'
import ModelName from './model-name' 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 Radio from '@/app/components/base/radio'
import Panel from '@/app/components/base/panel' import Panel from '@/app/components/base/panel'
import type { CompletionParams } from '@/models/debug' import type { CompletionParams } from '@/models/debug'
...@@ -25,21 +27,23 @@ import Loading from '@/app/components/base/loading' ...@@ -25,21 +27,23 @@ import Loading from '@/app/components/base/loading'
import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector' 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 { ModelType, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import type { ModelModeType } from '@/types/app'
export type IConfigModelProps = { export type IConfigModelProps = {
isAdvancedMode: boolean
mode: string mode: string
modelId: string modelId: string
provider: ProviderEnum provider: ProviderEnum
setModelId: (id: string, provider: ProviderEnum) => void setModel: (model: { id: string; provider: ProviderEnum; mode: ModelModeType }) => void
completionParams: CompletionParams completionParams: CompletionParams
onCompletionParamsChange: (newParams: CompletionParams) => void onCompletionParamsChange: (newParams: CompletionParams) => void
disabled: boolean disabled: boolean
} }
const ConfigModel: FC<IConfigModelProps> = ({ const ConfigModel: FC<IConfigModelProps> = ({
isAdvancedMode,
modelId, modelId,
provider, provider,
setModelId, setModel,
completionParams, completionParams,
onCompletionParamsChange, onCompletionParamsChange,
disabled, disabled,
...@@ -56,6 +60,8 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -56,6 +60,8 @@ const ConfigModel: FC<IConfigModelProps> = ({
const hasEnableParams = currParams && Object.keys(currParams).some(key => currParams[key].enabled) const hasEnableParams = currParams && Object.keys(currParams).some(key => currParams[key].enabled)
const allSupportParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty', 'max_tokens'] const allSupportParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty', 'max_tokens']
const currSupportParams = currParams ? allSupportParams.filter(key => currParams[key].enabled) : allSupportParams const currSupportParams = currParams ? allSupportParams.filter(key => currParams[key].enabled) : allSupportParams
if (isAdvancedMode)
currSupportParams.push('stop')
useEffect(() => { useEffect(() => {
(async () => { (async () => {
...@@ -115,11 +121,15 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -115,11 +121,15 @@ const ConfigModel: FC<IConfigModelProps> = ({
return adjustedValue return adjustedValue
} }
const handleSelectModel = (id: string, nextProvider = ProviderEnum.openai) => { const handleSelectModel = ({ id, provider: nextProvider, mode }: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
return async () => { return async () => {
const prevParamsRule = getAllParams()[provider]?.[modelId] const prevParamsRule = getAllParams()[provider]?.[modelId]
setModelId(id, nextProvider) setModel({
id,
provider: nextProvider || ProviderEnum.openai,
mode,
})
await ensureModelParamLoaded(nextProvider, id) await ensureModelParamLoaded(nextProvider, id)
...@@ -211,24 +221,34 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -211,24 +221,34 @@ const ConfigModel: FC<IConfigModelProps> = ({
setToneId(matchToneId(completionParams)) setToneId(matchToneId(completionParams))
}, [completionParams]) }, [completionParams])
const handleParamChange = (key: string, value: number) => { const handleParamChange = (key: string, value: number | string[]) => {
if (value === undefined)
return
if (key === 'stop') {
onCompletionParamsChange({
...completionParams,
[key]: value as string[],
})
}
else {
const currParamsRule = getAllParams()[provider]?.[modelId] const currParamsRule = getAllParams()[provider]?.[modelId]
let notOutRangeValue = parseFloat((value || 0).toFixed(2)) let notOutRangeValue = parseFloat((value as number).toFixed(2))
notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue) notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue)
notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue) notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue)
onCompletionParamsChange({ onCompletionParamsChange({
...completionParams, ...completionParams,
[key]: notOutRangeValue, [key]: notOutRangeValue,
}) })
} }
}
const ableStyle = 'bg-indigo-25 border-[#2A87F5] cursor-pointer' const ableStyle = 'bg-indigo-25 border-[#2A87F5] cursor-pointer'
const diabledStyle = 'bg-[#FFFCF5] border-[#F79009]' const diabledStyle = 'bg-[#FFFCF5] border-[#F79009]'
const getToneIcon = (toneId: number) => { const getToneIcon = (toneId: number) => {
const className = 'w-[14px] h-[14px]' const className = 'w-[14px] h-[14px]'
const res = ({ const res = ({
1: <Brush01 className={className}/>, 1: <Brush01 className={className} />,
2: <Scales02 className={className} />, 2: <Scales02 className={className} />,
3: <Target04 className={className} />, 3: <Target04 className={className} />,
4: <Sliders02 className={className} />, 4: <Sliders02 className={className} />,
...@@ -249,17 +269,19 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -249,17 +269,19 @@ const ConfigModel: FC<IConfigModelProps> = ({
return ( return (
<div className='relative' ref={configContentRef}> <div className='relative' ref={configContentRef}>
<div <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()} onClick={() => !disabled && toogleShowConfig()}
> >
<ModelIcon <ModelIcon
className='!w-5 !h-5'
modelId={modelId} modelId={modelId}
providerName={provider} providerName={provider}
/> />
<div className='text-[13px] text-gray-900 font-medium'> <div className='text-[13px] text-gray-900 font-medium'>
<ModelName modelId={selectedModel.name} modelDisplayName={currModel?.model_display_name} /> <ModelName modelId={selectedModel.name} modelDisplayName={currModel?.model_display_name} />
</div> </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> </div>
{isShowConfig && ( {isShowConfig && (
<Panel <Panel
...@@ -282,6 +304,8 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -282,6 +304,8 @@ const ConfigModel: FC<IConfigModelProps> = ({
<div className="flex items-center justify-between my-5 h-9"> <div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div> <div>{t('appDebug.modelConfig.model')}</div>
<ModelSelector <ModelSelector
isShowModelModeType={isAdvancedMode}
isShowAddModel
popClassName='right-0' popClassName='right-0'
triggerIconSmall triggerIconSmall
value={{ value={{
...@@ -290,7 +314,11 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -290,7 +314,11 @@ const ConfigModel: FC<IConfigModelProps> = ({
}} }}
modelType={ModelType.textGeneration} modelType={ModelType.textGeneration}
onChange={(model) => { 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> </div>
...@@ -343,20 +371,21 @@ const ConfigModel: FC<IConfigModelProps> = ({ ...@@ -343,20 +371,21 @@ const ConfigModel: FC<IConfigModelProps> = ({
{/* Params */} {/* Params */}
<div className={cn(hasEnableParams && 'mt-4', 'space-y-4', !allParams[provider]?.[modelId] && 'flex items-center min-h-[200px]')}> <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 currSupportParams.map(key => (<ParamItem
key={key} key={key}
id={key} id={key}
name={t(`common.model.params.${key}`)} name={t(`common.model.params.${key === 'stop' ? 'stop_sequences' : key}`)}
tip={t(`common.model.params.${key}Tip`)} tip={t(`common.model.params.${key === 'stop' ? 'stop_sequences' : key}Tip`)}
{...currParams[key] as any} {...currParams[key] as any}
value={(completionParams as any)[key] as any} value={(completionParams as any)[key] as any}
onChange={handleParamChange} onChange={handleParamChange}
inputType={key === 'stop' ? 'inputTag' : 'slider'}
/>)) />))
) )
: ( : (
<Loading type='area'/> <Loading type='area' />
)} )}
</div> </div>
</div> </div>
......
'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)
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Slider from '@/app/components/base/slider' import Slider from '@/app/components/base/slider'
import TagInput from '@/app/components/base/tag-input'
export const getFitPrecisionValue = (num: number, precision: number | null) => { export const getFitPrecisionValue = (num: number, precision: number | null) => {
if (!precision || !(`${num}`).includes('.')) if (!precision || !(`${num}`).includes('.'))
...@@ -19,16 +21,19 @@ export type IParamIteProps = { ...@@ -19,16 +21,19 @@ export type IParamIteProps = {
id: string id: string
name: string name: string
tip: string tip: string
value: number value: number | string[]
step?: number step?: number
min?: number min?: number
max: number max: number
precision: number | null precision: number | null
onChange: (key: string, value: number) => void onChange: (key: string, value: number | string[]) => void
inputType?: 'inputTag' | 'slider'
} }
const TIMES_TEMPLATE = '1000000000000' 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) => { const getToIntTimes = (num: number) => {
if (precision) if (precision)
return parseInt(TIMES_TEMPLATE.slice(0, precision + 1), 10) return parseInt(TIMES_TEMPLATE.slice(0, precision + 1), 10)
...@@ -45,6 +50,7 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max ...@@ -45,6 +50,7 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max
}, [value, precision]) }, [value, precision])
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col flex-shrink-0">
<div className="flex items-center"> <div className="flex items-center">
<span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span> <span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
{/* Give tooltip different tip to avoiding hide bug */} {/* Give tooltip different tip to avoiding hide bug */}
...@@ -54,7 +60,17 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max ...@@ -54,7 +60,17 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max
</svg> </svg>
</Tooltip> </Tooltip>
</div> </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="flex items-center">
{inputType === 'inputTag'
? <TagInput
items={(value ?? []) as string[]}
onChange={newSequences => onChange(id, newSequences)}
customizedConfirmKey='Tab'
/>
: (
<>
<div className="mr-4 w-[120px]"> <div className="mr-4 w-[120px]">
<Slider value={value * times} min={min * times} max={max * times} onChange={(value) => { <Slider value={value * times} min={min * times} max={max * times} onChange={(value) => {
onChange(id, value / times) onChange(id, value / times)
...@@ -69,6 +85,9 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max ...@@ -69,6 +85,9 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max
value = max value = max
onChange(id, value) onChange(id, value)
}} /> }} />
</>
)
}
</div> </div>
</div> </div>
) )
......
'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)
'use client' 'use client'
import React, { FC, useRef } from 'react' import type { FC } from 'react'
import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { useClickAway } from 'ahooks'
import VarHighlight from '../../base/var-highlight' import VarHighlight from '../../base/var-highlight'
import Button from '@/app/components/base/button'
export interface IConfirmAddVarProps { export type IConfirmAddVarProps = {
varNameArr: string[] varNameArr: string[]
onConfrim: () => void onConfrim: () => void
onCancel: () => void onCancel: () => void
...@@ -28,19 +28,20 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({ ...@@ -28,19 +28,20 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const mainContentRef = useRef<HTMLDivElement>(null) const mainContentRef = useRef<HTMLDivElement>(null)
useClickAway(() => { // new prompt editor blur trigger click...
onHide() // useClickAway(() => {
}, mainContentRef) // onHide()
// }, mainContentRef)
return ( return (
<div className='absolute inset-0 flex items-center justify-center rounded-xl' <div className='absolute inset-0 flex items-center justify-center rounded-xl'
style={{ style={{
backgroundColor: 'rgba(35, 56, 118, 0.2)' backgroundColor: 'rgba(35, 56, 118, 0.2)',
}}> }}>
<div <div
ref={mainContentRef} ref={mainContentRef}
className='w-[420px] rounded-xl bg-gray-50 p-6' className='w-[420px] rounded-xl bg-gray-50 p-6'
style={{ 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'> <div className='flex items-start space-x-3'>
...@@ -48,13 +49,13 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({ ...@@ -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' className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
style={{ style={{
backgroundColor: 'rgba(255, 255, 255, 0.9)', 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> >{VarIcon}</div>
<div className='grow-1'> <div className='grow-1'>
<div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div> <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'> <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} /> <VarHighlight key={name} name={name} />
))} ))}
</div> </div>
......
'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)
'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)
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import SimplePromptInput from './simple-prompt-input'
import cn from 'classnames' import AdvancedMessageInput from '@/app/components/app/configuration/config-prompt/advanced-prompt-input'
import ConfirmAddVar from './confirm-add-var' import { PromptRole } from '@/models/debug'
import s from './style.module.css' import type { PromptItem, PromptVariable } from '@/models/debug'
import BlockInput from '@/app/components/base/block-input' import { type AppType, ModelModeType } from '@/types/app'
import type { PromptVariable } from '@/models/debug' import ConfigContext from '@/context/debug-configuration'
import Tooltip from '@/app/components/base/tooltip' import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import { AppType } from '@/types/app' import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config'
import { getNewVar } from '@/utils/var'
export type IPromptProps = { export type IPromptProps = {
mode: AppType mode: AppType
promptTemplate: string promptTemplate: string
promptVariables: PromptVariable[] promptVariables: PromptVariable[]
readonly?: boolean readonly?: boolean
onChange?: (promp: string, promptVariables: PromptVariable[]) => void onChange?: (prompt: string, promptVariables: PromptVariable[]) => void
} }
const Prompt: FC<IPromptProps> = ({ const Prompt: FC<IPromptProps> = ({
...@@ -28,73 +28,114 @@ const Prompt: FC<IPromptProps> = ({ ...@@ -28,73 +28,114 @@ const Prompt: FC<IPromptProps> = ({
onChange, onChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const promptVariablesObj = (() => {
const obj: Record<string, boolean> = {} const {
promptVariables.forEach((item) => { isAdvancedMode,
obj[item.key] = true currentAdvancedPrompt,
setCurrentAdvancedPrompt,
modelModeType,
} = useContext(ConfigContext)
const handleMessageTypeChange = (index: number, role: PromptRole) => {
const newPrompt = produce(currentAdvancedPrompt as PromptItem[], (draft) => {
draft[index].role = role
}) })
return obj setCurrentAdvancedPrompt(newPrompt)
})() }
const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables) const handleValueChange = (value: string, index?: number) => {
const [newTemplates, setNewTemplates] = React.useState('') if (modelModeType === ModelModeType.chat) {
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false) 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 handleChange = (newTemplates: string, keys: string[]) => { const handleAddMessage = () => {
// const hasRemovedKeysInput = promptVariables.filter(input => keys.includes(input.key)) const currentAdvancedPromptList = currentAdvancedPrompt as PromptItem[]
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key)) if (currentAdvancedPromptList.length === 0) {
if (newPromptVariables.length > 0) { setCurrentAdvancedPrompt([{
setNewPromptVariables(newPromptVariables) role: PromptRole.system,
setNewTemplates(newTemplates) text: '',
showConfirmAddVar() }])
return return
} }
onChange?.(newTemplates, []) const lastMessageType = currentAdvancedPromptList[currentAdvancedPromptList.length - 1].role
const appendMessage = {
role: lastMessageType === PromptRole.user ? PromptRole.assistant : PromptRole.user,
text: '',
} }
setCurrentAdvancedPrompt([...currentAdvancedPromptList, appendMessage])
const handleAutoAdd = (isAdd: boolean) => {
return () => {
onChange?.(newTemplates, isAdd ? newPromptVariables : [])
hideConfirmAddVar()
} }
const handlePromptDelete = (index: number) => {
const currentAdvancedPromptList = currentAdvancedPrompt as PromptItem[]
const newPrompt = produce(currentAdvancedPromptList, (draft) => {
draft.splice(index, 1)
})
setCurrentAdvancedPrompt(newPrompt)
} }
if (!isAdvancedMode) {
return ( return (
<div className={cn(!readonly ? `${s.gradientBorder}` : 'bg-gray-50', 'relative rounded-xl')}> <SimplePromptInput
<div className="flex items-center h-11 pl-3 gap-1"> mode={mode}
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg"> promptTemplate={promptTemplate}
<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" /> promptVariables={promptVariables}
</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>
<BlockInput
readonly={readonly} readonly={readonly}
value={promptTemplate} onChange={onChange}
onConfirm={(value: string, vars: string[]) => {
handleChange(value, vars)
}}
/> />
)
}
{isShowConfirmAddVar && ( return (
<ConfirmAddVar <div>
varNameArr={newPromptVariables.map(v => v.name)} <div className='space-y-3'>
onConfrim={handleAutoAdd(true)} {modelModeType === ModelModeType.chat
onCancel={handleAutoAdd(false)} ? (
onHide={hideConfirmAddVar} (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>
{(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> </div>
) )
......
'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)
'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)
...@@ -13,3 +13,11 @@ ...@@ -13,3 +13,11 @@
padding: 2px; padding: 2px;
box-sizing: border-box; box-sizing: border-box;
} }
.optionWrap {
display: none;
}
.boxHeader:hover .optionWrap {
display: flex;
}
\ No newline at end of file
...@@ -73,7 +73,6 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar ...@@ -73,7 +73,6 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
delete newItem.max_length delete newItem.max_length
delete newItem.options delete newItem.options
} }
console.log(newItem)
return newItem return newItem
} }
...@@ -175,8 +174,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar ...@@ -175,8 +174,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
<Tooltip htmlContent={<div className='w-[180px]'> <Tooltip htmlContent={<div className='w-[180px]'>
{t('appDebug.variableTip')} {t('appDebug.variableTip')}
</div>} selector='config-var-tooltip'> </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> </Tooltip>
)} )}
</div> </div>
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
export type IAutomaticBtnProps = { export type IAutomaticBtnProps = {
onClick: () => void onClick: () => void
...@@ -22,12 +21,12 @@ const AutomaticBtn: FC<IAutomaticBtnProps> = ({ ...@@ -22,12 +21,12 @@ const AutomaticBtn: FC<IAutomaticBtnProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( 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} onClick={onClick}
> >
{leftIcon} {leftIcon}
<span className='text-xs font-semibold text-primary-600 uppercase'>{t('appDebug.operation.automatic')}</span> <span className='text-xs font-semibold text-indigo-600 uppercase'>{t('appDebug.operation.automatic')}</span>
</Button> </div>
) )
} }
export default React.memo(AutomaticBtn) export default React.memo(AutomaticBtn)
'use client' 'use client'
import React, { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/solid' import { PlusIcon } from '@heroicons/react/24/solid'
export interface IAddFeatureBtnProps { export type IAddFeatureBtnProps = {
toBottomHeight: number
onClick: () => void onClick: () => void
} }
const ITEM_HEIGHT = 48
const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({ const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
onClick toBottomHeight,
onClick,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div <div
className=' className='absolute z-[9] left-0 right-0 flex justify-center pb-4'
flex items-center h-8 space-x-2 px-3 style={{
top: toBottomHeight - ITEM_HEIGHT,
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)',
}}
>
<div
className='flex items-center h-8 space-x-2 px-3
border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer
text-xs font-semibold text-primary-600 uppercase text-xs font-semibold text-primary-600 uppercase
' '
style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}}
onClick={onClick} onClick={onClick}
> >
<PlusIcon className='w-4 h-4 font-semibold' /> <PlusIcon className='w-4 h-4 font-semibold' />
<div>{t('appDebug.operation.addFeature')}</div> <div>{t('appDebug.operation.addFeature')}</div>
</div> </div>
</div>
) )
} }
export default React.memo(AddFeatureBtn) export default React.memo(AddFeatureBtn)
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useRef } from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer' import produce from 'immer'
import { useBoolean } from 'ahooks' import { useBoolean, useScroll } from 'ahooks'
import DatasetConfig from '../dataset-config' import DatasetConfig from '../dataset-config'
import ChatGroup from '../features/chat-group' import ChatGroup from '../features/chat-group'
import ExperienceEnchanceGroup from '../features/experience-enchance-group' import ExperienceEnchanceGroup from '../features/experience-enchance-group'
import Toolbox from '../toolbox' import Toolbox from '../toolbox'
import HistoryPanel from '../config-prompt/conversation-histroy/history-panel'
import AddFeatureBtn from './feature/add-feature-btn' 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 ChooseFeature from './feature/choose-feature'
import useFeature from './feature/use-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 ConfigContext from '@/context/debug-configuration'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt' import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var' import ConfigVar from '@/app/components/app/configuration/config-var'
import type { PromptVariable } from '@/models/debug' import type { PromptVariable } from '@/models/debug'
import { AppType } from '@/types/app' import { AppType, ModelModeType } from '@/types/app'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
const Config: FC = () => { const Config: FC = () => {
const { const {
mode, mode,
isAdvancedMode,
modelModeType,
canReturnToSimpleMode,
hasSetBlockStatus,
showHistoryModal,
introduction, introduction,
setIntroduction, setIntroduction,
modelConfig, modelConfig,
...@@ -44,6 +47,7 @@ const Config: FC = () => { ...@@ -44,6 +47,7 @@ const Config: FC = () => {
const promptTemplate = modelConfig.configs.prompt_template const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables const promptVariables = modelConfig.configs.prompt_variables
// simple mode
const handlePromptChange = (newTemplate: string, newVariables: PromptVariable[]) => { const handlePromptChange = (newTemplate: string, newVariables: PromptVariable[]) => {
const newModelConfig = produce(modelConfig, (draft) => { const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = newTemplate draft.configs.prompt_template = newTemplate
...@@ -101,26 +105,29 @@ const Config: FC = () => { ...@@ -101,26 +105,29 @@ const Config: FC = () => {
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation) const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation)
const hasToolbox = false const hasToolbox = false
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const wrapRef = useRef<HTMLDivElement>(null)
const handleAutomaticRes = (res: AutomaticRes) => { const wrapScroll = useScroll(wrapRef)
const newModelConfig = produce(modelConfig, (draft) => { const toBottomHeight = (() => {
draft.configs.prompt_template = res.prompt if (!wrapRef.current)
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true })) return 999
}) const elem = wrapRef.current
setModelConfig(newModelConfig) const { clientHeight } = elem
setPrevPromptConfig(modelConfig.configs) const value = (wrapScroll?.top || 0) + clientHeight
if (mode === AppType.chat) return value
setIntroduction(res.opening_statement) })()
showAutomaticFalse()
}
return ( return (
<> <>
<div className="pb-[20px]"> <div
<div className='flex justify-between items-center mb-4'> ref={wrapRef}
<AddFeatureBtn onClick={showChooseFeatureTrue} /> className="relative px-6 pb-[50px] overflow-y-auto h-full"
<AutomaticBtn onClick={showAutomaticTrue}/> >
</div> <AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
{
(isAdvancedMode && canReturnToSimpleMode) && (
<AdvancedModeWaring />
)
}
{showChooseFeature && ( {showChooseFeature && (
<ChooseFeature <ChooseFeature
isShow={showChooseFeature} isShow={showChooseFeature}
...@@ -131,14 +138,7 @@ const Config: FC = () => { ...@@ -131,14 +138,7 @@ const Config: FC = () => {
showSpeechToTextItem={!!speech2textDefaultModel} showSpeechToTextItem={!!speech2textDefaultModel}
/> />
)} )}
{showAutomatic && (
<GetAutomaticResModal
mode={mode as AppType}
isShow={showAutomatic}
onClose={showAutomaticFalse}
onFinished={handleAutomaticRes}
/>
)}
{/* Template */} {/* Template */}
<ConfigPrompt <ConfigPrompt
mode={mode as AppType} mode={mode as AppType}
...@@ -156,6 +156,14 @@ const Config: FC = () => { ...@@ -156,6 +156,14 @@ const Config: FC = () => {
{/* Dataset */} {/* Dataset */}
<DatasetConfig /> <DatasetConfig />
{/* Chat History */}
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
<HistoryPanel
showWarning={!hasSetBlockStatus.history}
onShowEditModal={showHistoryModal}
/>
)}
{/* ChatConifig */} {/* ChatConifig */}
{ {
hasChatConfig && ( hasChatConfig && (
......
...@@ -3,16 +3,13 @@ import type { FC } from 'react' ...@@ -3,16 +3,13 @@ import type { FC } from 'react'
import React from 'react' import React 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 { useBoolean } from 'ahooks'
import { isEqual } from 'lodash-es'
import produce from 'immer' import produce from 'immer'
import FeaturePanel from '../base/feature-panel' import FeaturePanel from '../base/feature-panel'
import OperationBtn from '../base/operation-btn' import OperationBtn from '../base/operation-btn'
import CardItem from './card-item' import CardItem from './card-item'
import SelectDataSet from './select-dataset' import ParamsConfig from './params-config'
import ContextVar from './context-var' import ContextVar from './context-var'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import type { DataSet } from '@/models/datasets'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
const Icon = ( const Icon = (
...@@ -31,35 +28,12 @@ const DatasetConfig: FC = () => { ...@@ -31,35 +28,12 @@ const DatasetConfig: FC = () => {
setFormattingChanged, setFormattingChanged,
modelConfig, modelConfig,
setModelConfig, setModelConfig,
showSelectDataSet,
} = useContext(ConfigContext) } = useContext(ConfigContext)
const selectedIds = dataSet.map(item => item.id) const selectedIds = dataSet.map(item => item.id)
const hasData = dataSet.length > 0 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) => { const onRemove = (id: string) => {
setDataSet(dataSet.filter(item => item.id !== id)) setDataSet(dataSet.filter(item => item.id !== id))
setFormattingChanged(true) setFormattingChanged(true)
...@@ -89,7 +63,12 @@ const DatasetConfig: FC = () => { ...@@ -89,7 +63,12 @@ const DatasetConfig: FC = () => {
className='mt-3' className='mt-3'
headerIcon={Icon} headerIcon={Icon}
title={t('appDebug.feature.dataSet.title')} 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} hasHeaderBottomBorder={!hasData}
noBodySpacing noBodySpacing
> >
...@@ -120,14 +99,6 @@ const DatasetConfig: FC = () => { ...@@ -120,14 +99,6 @@ const DatasetConfig: FC = () => {
/> />
)} )}
{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={hideSelectDataSet}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}
</FeaturePanel> </FeaturePanel>
) )
} }
......
'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)
...@@ -94,6 +94,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({ ...@@ -94,6 +94,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
isShow={isShow} isShow={isShow}
onClose={onClose} onClose={onClose}
className='w-[400px]' className='w-[400px]'
wrapperClassName='!z-[101]'
title={t('appDebug.feature.dataSet.selectTitle')} title={t('appDebug.feature.dataSet.selectTitle')}
> >
{!loaded && ( {!loaded && (
......
...@@ -11,7 +11,7 @@ import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api' ...@@ -11,7 +11,7 @@ import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed' import FormattingChanged from '../base/warning-mask/formatting-changed'
import GroupName from '../base/group-name' import GroupName from '../base/group-name'
import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset' 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 PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { IChatItem } from '@/app/components/app/chat/type' import type { IChatItem } from '@/app/components/app/chat/type'
import Chat from '@/app/components/app/chat' import Chat from '@/app/components/app/chat'
...@@ -37,6 +37,12 @@ const Debug: FC<IDebug> = ({ ...@@ -37,6 +37,12 @@ const Debug: FC<IDebug> = ({
const { const {
appId, appId,
mode, mode,
modelModeType,
hasSetBlockStatus,
isAdvancedMode,
promptMode,
chatPromptConfig,
completionPromptConfig,
introduction, introduction,
suggestedQuestionsAfterAnswerConfig, suggestedQuestionsAfterAnswerConfig,
speechToTextConfig, speechToTextConfig,
...@@ -53,6 +59,7 @@ const Debug: FC<IDebug> = ({ ...@@ -53,6 +59,7 @@ const Debug: FC<IDebug> = ({
modelConfig, modelConfig,
completionParams, completionParams,
hasSetContextVar, hasSetContextVar,
datasetConfigs,
} = useContext(ConfigContext) } = useContext(ConfigContext)
const { speech2textDefaultModel } = useProviderContext() const { speech2textDefaultModel } = useProviderContext()
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
...@@ -120,6 +127,18 @@ const Debug: FC<IDebug> = ({ ...@@ -120,6 +127,18 @@ const Debug: FC<IDebug> = ({
} }
const checkCanSend = () => { 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 = '' let hasEmptyInput = ''
const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required }) => { const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required }) => {
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
...@@ -155,11 +174,15 @@ const Debug: FC<IDebug> = ({ ...@@ -155,11 +174,15 @@ const Debug: FC<IDebug> = ({
id, id,
}, },
})) }))
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const postModelConfig: BackendModelConfig = { 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), user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: '', dataset_query_variable: contextVar || '',
opening_statement: introduction, opening_statement: introduction,
more_like_this: { more_like_this: {
enabled: false, enabled: false,
...@@ -174,8 +197,15 @@ const Debug: FC<IDebug> = ({ ...@@ -174,8 +197,15 @@ const Debug: FC<IDebug> = ({
model: { model: {
provider: modelConfig.provider, provider: modelConfig.provider,
name: modelConfig.model_id, name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any, completion_params: completionParams as any,
}, },
dataset_configs: datasetConfigs,
}
if (isAdvancedMode) {
postModelConfig.chat_prompt_config = chatPromptConfig
postModelConfig.completion_prompt_config = completionPromptConfig
} }
const data = { const data = {
...@@ -254,6 +284,11 @@ const Debug: FC<IDebug> = ({ ...@@ -254,6 +284,11 @@ const Debug: FC<IDebug> = ({
setChatList(produce(getChatList(), (draft) => { setChatList(produce(getChatList(), (draft) => {
const index = draft.findIndex(item => item.id === responseItem.id) const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) { if (index !== -1) {
const requestion = draft[index - 1]
draft[index - 1] = {
...requestion,
log: newResponseItem.message,
}
draft[index] = { draft[index] = {
...draft[index], ...draft[index],
more: { more: {
...@@ -326,7 +361,10 @@ const Debug: FC<IDebug> = ({ ...@@ -326,7 +361,10 @@ const Debug: FC<IDebug> = ({
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const postModelConfig: BackendModelConfig = { 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), user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '', dataset_query_variable: contextVar || '',
opening_statement: introduction, opening_statement: introduction,
...@@ -341,8 +379,15 @@ const Debug: FC<IDebug> = ({ ...@@ -341,8 +379,15 @@ const Debug: FC<IDebug> = ({
model: { model: {
provider: modelConfig.provider, provider: modelConfig.provider,
name: modelConfig.model_id, name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any, completion_params: completionParams as any,
}, },
dataset_configs: datasetConfigs,
}
if (isAdvancedMode) {
postModelConfig.chat_prompt_config = chatPromptConfig
postModelConfig.completion_prompt_config = completionPromptConfig
} }
const data = { const data = {
...@@ -413,6 +458,7 @@ const Debug: FC<IDebug> = ({ ...@@ -413,6 +458,7 @@ const Debug: FC<IDebug> = ({
isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel} isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
isShowCitation={citationConfig.enabled} isShowCitation={citationConfig.enabled}
isShowCitationHitInfo isShowCitationHitInfo
isShowPromptLog
/> />
</div> </div>
</div> </div>
...@@ -427,8 +473,11 @@ const Debug: FC<IDebug> = ({ ...@@ -427,8 +473,11 @@ const Debug: FC<IDebug> = ({
className="mt-2" className="mt-2"
content={completionRes} content={completionRes}
isLoading={!completionRes && isResponsing} isLoading={!completionRes && isResponsing}
isResponsing={isResponsing}
isInstalledApp={false} isInstalledApp={false}
messageId={messageId} messageId={messageId}
isError={false}
onRetry={() => { }}
/> />
)} )}
</div> </div>
......
'use client' 'use client'
import React, { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel' 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 SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
const SuggestedQuestionsAfterAnswer: FC = () => { const SuggestedQuestionsAfterAnswer: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -16,9 +18,7 @@ const SuggestedQuestionsAfterAnswer: FC = () => { ...@@ -16,9 +18,7 @@ const SuggestedQuestionsAfterAnswer: FC = () => {
<Tooltip htmlContent={<div className='w-[180px]'> <Tooltip htmlContent={<div className='w-[180px]'>
{t('appDebug.feature.suggestedQuestionsAfterAnswer.description')} {t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
</div>} selector='suggestion-question-tooltip'> </div>} selector='suggestion-question-tooltip'>
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <HelpCircle className='w-[14px] h-[14px] text-gray-400' />
<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> </Tooltip>
</div> </div>
} }
......
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
...@@ -7,9 +7,13 @@ import { usePathname } from 'next/navigation' ...@@ -7,9 +7,13 @@ import { usePathname } from 'next/navigation'
import produce from 'immer' import produce from 'immer'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import cn from 'classnames' import cn from 'classnames'
import { clone, isEqual } from 'lodash-es'
import Button from '../../base/button' import Button from '../../base/button'
import Loading from '../../base/loading' 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 { DataSet } from '@/models/datasets'
import type { ModelConfig as BackendModelConfig } from '@/types/app' import type { ModelConfig as BackendModelConfig } from '@/types/app'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
...@@ -24,7 +28,11 @@ import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from ...@@ -24,7 +28,11 @@ import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from
import { fetchDatasets } from '@/service/datasets' import { fetchDatasets } from '@/service/datasets'
import AccountSetting from '@/app/components/header/account-setting' import AccountSetting from '@/app/components/header/account-setting'
import { useProviderContext } from '@/context/provider-context' 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 = { type PublichConfig = {
modelConfig: ModelConfig modelConfig: ModelConfig
...@@ -44,6 +52,7 @@ const Configuration: FC = () => { ...@@ -44,6 +52,7 @@ const Configuration: FC = () => {
const [publishedConfig, setPublishedConfig] = useState<PublichConfig | null>(null) const [publishedConfig, setPublishedConfig] = useState<PublichConfig | null>(null)
const [conversationId, setConversationId] = useState<string | null>('') const [conversationId, setConversationId] = useState<string | null>('')
const [introduction, setIntroduction] = useState<string>('') const [introduction, setIntroduction] = useState<string>('')
const [controlClearChatMessage, setControlClearChatMessage] = useState(0) const [controlClearChatMessage, setControlClearChatMessage] = useState(0)
const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({ const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({
...@@ -75,6 +84,7 @@ const Configuration: FC = () => { ...@@ -75,6 +84,7 @@ const Configuration: FC = () => {
const [modelConfig, doSetModelConfig] = useState<ModelConfig>({ const [modelConfig, doSetModelConfig] = useState<ModelConfig>({
provider: ProviderEnum.openai, provider: ProviderEnum.openai,
model_id: 'gpt-3.5-turbo', model_id: 'gpt-3.5-turbo',
mode: ModelModeType.unset,
configs: { configs: {
prompt_template: '', prompt_template: '',
prompt_variables: [] as PromptVariable[], prompt_variables: [] as PromptVariable[],
...@@ -87,21 +97,52 @@ const Configuration: FC = () => { ...@@ -87,21 +97,52 @@ const Configuration: FC = () => {
dataSets: [], dataSets: [],
}) })
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>({
top_k: 2,
score_threshold: {
enable: false,
value: 0.7,
},
})
const setModelConfig = (newModelConfig: ModelConfig) => { const setModelConfig = (newModelConfig: ModelConfig) => {
doSetModelConfig(newModelConfig) doSetModelConfig(newModelConfig)
} }
const setModelId = (modelId: string, provider: ProviderEnum) => { const modelModeType = modelConfig.mode
const newModelConfig = produce(modelConfig, (draft: any) => {
draft.provider = provider
draft.model_id = modelId
})
setModelConfig(newModelConfig)
}
const [dataSets, setDataSets] = useState<DataSet[]>([]) const [dataSets, setDataSets] = useState<DataSet[]>([])
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const hasSetContextVar = !!contextVar 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 syncToPublishedConfig = (_publishedConfig: PublichConfig) => {
const modelConfig = _publishedConfig.modelConfig const modelConfig = _publishedConfig.modelConfig
setModelConfig(_publishedConfig.modelConfig) setModelConfig(_publishedConfig.modelConfig)
...@@ -140,14 +181,101 @@ const Configuration: FC = () => { ...@@ -140,14 +181,101 @@ const Configuration: FC = () => {
return quota_used === quota_limit 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 hasSetAPIKEY = hasSetCustomAPIKEY || !isTrailFinished
const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean() 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(() => { useEffect(() => {
fetchAppDetail({ url: '/apps', id: appId }).then(async (res) => { fetchAppDetail({ url: '/apps', id: appId }).then(async (res) => {
setMode(res.mode) setMode(res.mode)
const modelConfig = res.model_config 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 const model = res.model_config.model
let datasets: any = null let datasets: any = null
...@@ -177,6 +305,7 @@ const Configuration: FC = () => { ...@@ -177,6 +305,7 @@ const Configuration: FC = () => {
modelConfig: { modelConfig: {
provider: model.provider, provider: model.provider,
model_id: model.name, model_id: model.name,
mode: model.mode,
configs: { configs: {
prompt_template: modelConfig.pre_prompt, prompt_template: modelConfig.pre_prompt,
prompt_variables: userInputsFormToPromptVariables(modelConfig.user_input_form, modelConfig.dataset_query_variable), prompt_variables: userInputsFormToPromptVariables(modelConfig.user_input_form, modelConfig.dataset_query_variable),
...@@ -197,10 +326,38 @@ const Configuration: FC = () => { ...@@ -197,10 +326,38 @@ const Configuration: FC = () => {
}) })
}, [appId]) }, [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 contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
const cannotPublish = promptEmpty || contextVarEmpty const handlePublish = async (isSilence?: boolean) => {
const saveAppConfig = async () => {
const modelId = modelConfig.model_id const modelId = modelConfig.model_id
const promptTemplate = modelConfig.configs.prompt_template const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables const promptVariables = modelConfig.configs.prompt_variables
...@@ -209,6 +366,18 @@ const Configuration: FC = () => { ...@@ -209,6 +366,18 @@ const Configuration: FC = () => {
notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty'), duration: 3000 }) notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty'), duration: 3000 })
return 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) { if (contextVarEmpty) {
notify({ type: 'error', message: t('appDebug.feature.dataSet.queryVariable.contextVarNotEmpty'), duration: 3000 }) notify({ type: 'error', message: t('appDebug.feature.dataSet.queryVariable.contextVarNotEmpty'), duration: 3000 })
return return
...@@ -222,7 +391,11 @@ const Configuration: FC = () => { ...@@ -222,7 +391,11 @@ const Configuration: FC = () => {
// new model config data struct // new model config data struct
const data: BackendModelConfig = { 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), user_input_form: promptVariablesToUserInputsForm(promptVariables),
dataset_query_variable: contextVar || '', dataset_query_variable: contextVar || '',
opening_statement: introduction || '', opening_statement: introduction || '',
...@@ -237,8 +410,15 @@ const Configuration: FC = () => { ...@@ -237,8 +410,15 @@ const Configuration: FC = () => {
model: { model: {
provider: modelConfig.provider, provider: modelConfig.provider,
name: modelId, name: modelId,
mode: modelConfig.mode,
completion_params: completionParams as any, 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 }) await updateAppModelConfig({ url: `/apps/${appId}/model-config`, body: data })
...@@ -254,7 +434,11 @@ const Configuration: FC = () => { ...@@ -254,7 +434,11 @@ const Configuration: FC = () => {
modelConfig: newModelConfig, modelConfig: newModelConfig,
completionParams, completionParams,
}) })
if (!isSilence)
notify({ type: 'success', message: t('common.api.success'), duration: 3000 }) notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
setCanReturnToSimpleMode(false)
return true
} }
const [showConfirm, setShowConfirm] = useState(false) const [showConfirm, setShowConfirm] = useState(false)
...@@ -278,6 +462,20 @@ const Configuration: FC = () => { ...@@ -278,6 +462,20 @@ const Configuration: FC = () => {
hasSetAPIKEY, hasSetAPIKEY,
isTrailFinished, isTrailFinished,
mode, mode,
modelModeType,
promptMode,
isAdvancedMode,
setPromptMode,
canReturnToSimpleMode,
setCanReturnToSimpleMode,
chatPromptConfig,
completionPromptConfig,
currentAdvancedPrompt,
setCurrentAdvancedPrompt,
conversationHistoriesRole: completionPromptConfig.conversation_histories_role,
showHistoryModal,
setConversationHistoriesRole,
hasSetBlockStatus,
conversationId, conversationId,
introduction, introduction,
setIntroduction, setIntroduction,
...@@ -304,23 +502,56 @@ const Configuration: FC = () => { ...@@ -304,23 +502,56 @@ const Configuration: FC = () => {
setCompletionParams, setCompletionParams,
modelConfig, modelConfig,
setModelConfig, setModelConfig,
showSelectDataSet,
dataSets, dataSets,
setDataSets, setDataSets,
datasetConfigs,
setDatasetConfigs,
hasSetContextVar, hasSetContextVar,
}} }}
> >
<> <>
<div className="flex flex-col h-full"> <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='flex items-center justify-between px-6 shrink-0 h-14'>
<div className='text-xl text-gray-900'>{t('appDebug.pageTitle')}</div> <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'> <div className='flex items-center'>
{/* Model and Parameters */} {/* Model and Parameters */}
<ConfigModel <ConfigModel
isAdvancedMode={isAdvancedMode}
mode={mode} mode={mode}
provider={modelConfig.provider as ProviderEnum} provider={modelConfig.provider as ProviderEnum}
completionParams={completionParams} completionParams={completionParams}
modelId={modelConfig.model_id} modelId={modelConfig.model_id}
setModelId={setModelId} setModel={setModel}
onCompletionParamsChange={(newParams: CompletionParams) => { onCompletionParamsChange={(newParams: CompletionParams) => {
setCompletionParams(newParams) setCompletionParams(newParams)
}} }}
...@@ -328,14 +559,14 @@ const Configuration: FC = () => { ...@@ -328,14 +559,14 @@ const Configuration: FC = () => {
/> />
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div> <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 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> </div>
<div className='flex grow h-[200px]'> <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 /> <Config />
</div> </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} /> <Debug hasSetAPIKEY={hasSetAPIKEY} onSetting={showSetAPIKey} />
</div> </div>
</div> </div>
...@@ -373,6 +604,28 @@ const Configuration: FC = () => { ...@@ -373,6 +604,28 @@ const Configuration: FC = () => {
{isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => { {isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
hideSetAPIkey() 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> </ConfigContext.Provider>
) )
......
'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)
...@@ -6,10 +6,9 @@ import { useContext } from 'use-context-selector' ...@@ -6,10 +6,9 @@ import { useContext } from 'use-context-selector'
import { import {
PlayIcon, PlayIcon,
} from '@heroicons/react/24/solid' } 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 ConfigContext from '@/context/debug-configuration'
import type { PromptVariable } from '@/models/debug' 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 Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
...@@ -21,23 +20,13 @@ export type IPromptValuePanelProps = { ...@@ -21,23 +20,13 @@ export type IPromptValuePanelProps = {
onSend?: () => void 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> = ({ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
appType, appType,
onSend, onSend,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { modelConfig, inputs, setInputs, mode } = useContext(ConfigContext) const { modelModeType, modelConfig, inputs, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
const [promptPreviewCollapse, setPromptPreviewCollapse] = useState(false)
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false) const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => { const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
return key && key?.trim() && name && name?.trim() return key && key?.trim() && name && name?.trim()
}) })
...@@ -50,7 +39,18 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({ ...@@ -50,7 +39,18 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
return obj 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 = () => { const renderRunButton = () => {
return ( return (
<Button <Button
...@@ -83,61 +83,21 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({ ...@@ -83,61 +83,21 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
setInputs(newInputs) 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 ( return (
<div className="pb-3 border border-gray-200 bg-white rounded-xl" style={{ <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)', 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={'mt-3 px-4 bg-white'}>
<div className={ <div className={
`${!userInputFieldCollapse && 'mb-2'}` `${!userInputFieldCollapse && 'mb-2'}`
}> }>
<div className='flex items-center space-x-1 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}> <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 userInputFieldCollapse
? <ChevronRight 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-700' /> : <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> </div>
{appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && ( {appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
<div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div> <div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
...@@ -150,8 +110,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({ ...@@ -150,8 +110,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
? ( ? (
<div className="space-y-3 "> <div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => ( {promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="flex justify-between"> <div key={key} className="xl:flex justify-between">
<div className="mr-1 pt-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div> <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
{type === 'select' && ( {type === 'select' && (
<Select <Select
className='w-full' className='w-full'
......
.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);
}
...@@ -9,14 +9,14 @@ import { ...@@ -9,14 +9,14 @@ import {
InformationCircleIcon, InformationCircleIcon,
XMarkIcon, XMarkIcon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { SparklesIcon } from '@heroicons/react/24/solid'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from 'react-infinite-scroll-component'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { createContext, useContext } from 'use-context-selector' import { createContext, useContext } from 'use-context-selector'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css' import s from './style.module.css'
import VarPanel from './var-panel'
import { randomString } from '@/utils' import { randomString } from '@/utils'
import { EditIconSolid } from '@/app/components/app/chat/icon-component' import { EditIconSolid } from '@/app/components/app/chat/icon-component'
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type' import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
...@@ -32,6 +32,8 @@ import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversa ...@@ -32,6 +32,8 @@ import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversa
import { TONE_LIST } from '@/config' import { TONE_LIST } from '@/config'
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon' import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
import ModelName from '@/app/components/app/configuration/config-model/model-name' 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 = { type IConversationList = {
logs?: ChatConversationsResponse | CompletionConversationsResponse logs?: ChatConversationsResponse | CompletionConversationsResponse
...@@ -78,6 +80,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => { ...@@ -78,6 +80,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
id: `question-${item.id}`, id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false, isAnswer: false,
log: item.message as any,
}) })
newChatList.push({ newChatList.push({
...@@ -102,7 +105,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => { ...@@ -102,7 +105,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty'] const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
type IDetailPanel<T> = { type IDetailPanel<T> = {
detail: T detail: any
onFeedback: FeedbackFunc onFeedback: FeedbackFunc
onSubmitAnnotation: SubmitAnnotationFunc onSubmitAnnotation: SubmitAnnotationFunc
} }
...@@ -112,7 +115,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -112,7 +115,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
const { t } = useTranslation() const { t } = useTranslation()
const [items, setItems] = React.useState<IChatItem[]>([]) const [items, setItems] = React.useState<IChatItem[]>([])
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [varValues, setVarValues] = useState<Record<string, string>>({})
const fetchData = async () => { const fetchData = async () => {
try { try {
if (!hasMore) if (!hasMore)
...@@ -128,6 +131,10 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -128,6 +131,10 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
url: `/apps/${appDetail?.id}/chat-messages`, url: `/apps/${appDetail?.id}/chat-messages`,
params, params,
}) })
if (messageRes.data.length > 0) {
const varValues = messageRes.data[0].inputs
setVarValues(varValues)
}
const newItems = [...getFormattedChatList(messageRes.data), ...items] const newItems = [...getFormattedChatList(messageRes.data), ...items]
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) { if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
newItems.unshift({ newItems.unshift({
...@@ -153,7 +160,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -153,7 +160,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
const isChatMode = appDetail?.mode === 'chat' const isChatMode = appDetail?.mode === 'chat'
const targetTone = TONE_LIST.find((item) => { const targetTone = TONE_LIST.find((item: any) => {
let res = true let res = true
validatedParams.forEach((param) => { validatedParams.forEach((param) => {
res = item.config?.[param] === detail.model_config?.configs?.completion_params?.[param] res = item.config?.[param] === detail.model_config?.configs?.completion_params?.[param]
...@@ -161,20 +168,47 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -161,20 +168,47 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
return res return res
})?.name ?? 'custom' })?.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'> return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
{/* Panel Header */} {/* Panel Header */}
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'> <div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
<div className='flex-1'> <div>
<span className='text-gray-500 text-[10px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</span> <div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
<div className='text-gray-800 text-sm'>{isChatMode ? detail.id : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</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>
<div className='mr-2 bg-gray-50 py-1.5 px-2.5 rounded-lg flex items-center text-[13px]'> <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 <ModelIcon
className={classNames('mr-1.5', 'w-5 h-5')} className='!w-5 !h-5'
modelId={detail.model_config.model.name} modelId={modelName}
providerName={detail.model_config.model.provider} providerName={provideName}
/> />
<ModelName modelId={detail.model_config.model.name} modelDisplayName={detail.model_config.model.name} /> <div className='text-[13px] text-gray-900 font-medium'>
<ModelName modelId={modelName} modelDisplayName={modelName} />
</div>
<ModelModeTypeLabel type={ModelModeType.chat} isHighlight />
</div> </div>
<Popover <Popover
position='br' position='br'
...@@ -189,10 +223,10 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -189,10 +223,10 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
<span>Tone of responses</span> <span>Tone of responses</span>
<div>{targetTone}</div> <div>{targetTone}</div>
</div> </div>
{['temperature', 'top_p', 'presence_penalty', 'max_tokens'].map((param: string, index: number) => { {['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}> 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-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> <span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
</div> </div>
})} })}
</div>} </div>}
...@@ -201,13 +235,14 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -201,13 +235,14 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} /> <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
</div> </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')}
</div> </div>
<div className='text-gray-700 font-medium text-sm mt-2'>{detail.model_config?.pre_prompt || emptyText}</div> {/* Panel Body */}
{varList.length > 0 && (
<div className='px-6 pt-4 pb-2'>
<VarPanel varList={varList} />
</div> </div>
)}
{!isChatMode {!isChatMode
? <div className="px-2.5 py-4"> ? <div className="px-2.5 py-4">
<Chat <Chat
...@@ -216,6 +251,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -216,6 +251,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
onFeedback={onFeedback} onFeedback={onFeedback}
onSubmitAnnotation={onSubmitAnnotation} onSubmitAnnotation={onSubmitAnnotation}
displayScene='console' displayScene='console'
isShowPromptLog
/> />
</div> </div>
: items.length < 8 : items.length < 8
...@@ -226,6 +262,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -226,6 +262,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
onFeedback={onFeedback} onFeedback={onFeedback}
onSubmitAnnotation={onSubmitAnnotation} onSubmitAnnotation={onSubmitAnnotation}
displayScene='console' displayScene='console'
isShowPromptLog
/> />
</div> </div>
: <div : <div
...@@ -265,6 +302,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo ...@@ -265,6 +302,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
onFeedback={onFeedback} onFeedback={onFeedback}
onSubmitAnnotation={onSubmitAnnotation} onSubmitAnnotation={onSubmitAnnotation}
displayScene='console' displayScene='console'
isShowPromptLog
/> />
</InfiniteScroll> </InfiniteScroll>
</div> </div>
...@@ -382,7 +420,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) ...@@ -382,7 +420,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
className={(isHighlight && !isChatMode) ? '' : '!hidden'} className={(isHighlight && !isChatMode) ? '' : '!hidden'}
selector={`highlight-${randomString(16)}`} 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 || '-'} {value || '-'}
</div> </div>
</Tooltip> </Tooltip>
...@@ -413,9 +451,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) ...@@ -413,9 +451,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
</tr> </tr>
</thead> </thead>
<tbody className="text-gray-500"> <tbody className="text-gray-500">
{logs.data.map((log) => { {logs.data.map((log: any) => {
const endUser = log.from_end_user_session_id 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') const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
return <tr return <tr
key={log.id} key={log.id}
......
'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)
...@@ -165,7 +165,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({ ...@@ -165,7 +165,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div> </div>
{showEmojiPicker && <EmojiPicker {showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => { onSelect={(icon, icon_background) => {
console.log(icon, icon_background)
setEmoji({ icon, icon_background }) setEmoji({ icon, icon_background })
setShowEmojiPicker(false) setShowEmojiPicker(false)
}} }}
......
'use client' 'use client'
import type { FC } from 'react' import type { Dispatch, FC, SetStateAction } from 'react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { useParams } from 'next/navigation'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { HashtagIcon } from '@heroicons/react/24/solid' import { HashtagIcon } from '@heroicons/react/24/solid'
import PromptLog from '@/app/components/app/chat/log'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import type { Feedbacktype } from '@/app/components/app/chat/type' import type { Feedbacktype } from '@/app/components/app/chat/type'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share' 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 { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather' import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import { fetchTextGenerationMessge } from '@/service/debug'
const MAX_DEPTH = 3 const MAX_DEPTH = 3
export type IGenerationItemProps = { export type IGenerationItemProps = {
className?: string className?: string
...@@ -23,7 +26,9 @@ export type IGenerationItemProps = { ...@@ -23,7 +26,9 @@ export type IGenerationItemProps = {
onRetry: () => void onRetry: () => void
content: string content: string
messageId?: string | null messageId?: string | null
conversationId?: string
isLoading?: boolean isLoading?: boolean
isResponsing?: boolean
isInWebApp?: boolean isInWebApp?: boolean
moreLikeThis?: boolean moreLikeThis?: boolean
depth?: number depth?: number
...@@ -64,6 +69,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -64,6 +69,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
content, content,
messageId, messageId,
isLoading, isLoading,
isResponsing,
moreLikeThis, moreLikeThis,
isInWebApp = false, isInWebApp = false,
feedback, feedback,
...@@ -77,14 +83,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -77,14 +83,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
controlClearMoreLikeThis, controlClearMoreLikeThis,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const params = useParams()
const isTop = depth === 1 const isTop = depth === 1
const ref = useRef(null)
const [completionRes, setCompletionRes] = useState('') const [completionRes, setCompletionRes] = useState('')
const [childMessageId, setChildMessageId] = useState<string | null>(null) const [childMessageId, setChildMessageId] = useState<string | null>(null)
const hasChild = !!childMessageId const hasChild = !!childMessageId
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({ const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
rating: null, rating: null,
}) })
const [promptLog, setPromptLog] = useState<{ role: string; text: string }[]>([])
const handleFeedback = async (childFeedback: Feedbacktype) => { const handleFeedback = async (childFeedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId) await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
...@@ -150,8 +158,17 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -150,8 +158,17 @@ const GenerationItem: FC<IGenerationItemProps> = ({
setChildMessageId(null) setChildMessageId(null)
}, [isLoading]) }, [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 ( 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 style={isTop
? { ? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)', boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
...@@ -186,6 +203,26 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -186,6 +203,26 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<div className='flex items-center justify-between mt-3'> <div className='flex items-center justify-between mt-3'>
<div className='flex items-center'> <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 <SimpleBtn
isDisabled={isError || !messageId} isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1')} className={cn(isMobile && '!px-1.5', 'space-x-1')}
......
...@@ -4,9 +4,8 @@ import type { ChangeEvent, FC } from 'react' ...@@ -4,9 +4,8 @@ import type { ChangeEvent, FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '../toast'
import { varHighlightHTML } from '../../app/configuration/base/var-highlight' import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
import Button from '@/app/components/base/button' import Toast from '../toast'
import { checkKeys } from '@/utils/var' import { checkKeys } from '@/utils/var'
// regex to match the {{}} and replace it with a span // regex to match the {{}} and replace it with a span
...@@ -65,7 +64,7 @@ const BlockInput: FC<IBlockInputProps> = ({ ...@@ -65,7 +64,7 @@ const BlockInput: FC<IBlockInputProps> = ({
}, [isEditing]) }, [isEditing])
const style = classNames({ const style = classNames({
'block px-4 py-1 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true, 'block px-4 py-2 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true,
'block-input--editing': isEditing, 'block-input--editing': isEditing,
}) })
...@@ -76,9 +75,8 @@ const BlockInput: FC<IBlockInputProps> = ({ ...@@ -76,9 +75,8 @@ const BlockInput: FC<IBlockInputProps> = ({
.replace(/\n/g, '<br />') .replace(/\n/g, '<br />')
// Not use useCallback. That will cause out callback get old data. // Not use useCallback. That will cause out callback get old data.
const handleSubmit = () => { const handleSubmit = (value: string) => {
if (onConfirm) { if (onConfirm) {
const value = currentValue
const keys = getInputKeys(value) const keys = getInputKeys(value)
const { isValid, errorKey, errorMessageKey } = checkKeys(keys) const { isValid, errorKey, errorMessageKey } = checkKeys(keys)
if (!isValid) { if (!isValid) {
...@@ -89,17 +87,13 @@ const BlockInput: FC<IBlockInputProps> = ({ ...@@ -89,17 +87,13 @@ const BlockInput: FC<IBlockInputProps> = ({
return return
} }
onConfirm(value, keys) onConfirm(value, keys)
setIsEditing(false)
} }
} }
const handleCancel = useCallback(() => {
setIsEditing(false)
setCurrentValue(value)
}, [value])
const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => { const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setCurrentValue(e.target.value) const value = e.target.value
setCurrentValue(value)
handleSubmit(value)
}, []) }, [])
// Prevent rerendering caused cursor to jump to the start of the contentEditable element // Prevent rerendering caused cursor to jump to the start of the contentEditable element
...@@ -117,18 +111,16 @@ const BlockInput: FC<IBlockInputProps> = ({ ...@@ -117,18 +111,16 @@ const BlockInput: FC<IBlockInputProps> = ({
const textAreaContent = ( const textAreaContent = (
<div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}> <div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}>
{isEditing {isEditing
? <div className='h-full px-4 py-1'> ? <div className='h-full px-4 py-2'>
<textarea <textarea
ref={contentEditableRef} ref={contentEditableRef}
className={classNames(editAreaClassName, 'block w-full h-full absolut3e resize-none')} className={classNames(editAreaClassName, 'block w-full h-full resize-none')}
placeholder={placeholder} placeholder={placeholder}
onChange={onValueChange} onChange={onValueChange}
value={currentValue} value={currentValue}
onBlur={() => { onBlur={() => {
blur() blur()
if (!isContentChanged)
setIsEditing(false) setIsEditing(false)
// click confirm also make blur. Then outter value is change. So below code has problem. // click confirm also make blur. Then outter value is change. So below code has problem.
// setTimeout(() => { // setTimeout(() => {
// handleCancel() // handleCancel()
...@@ -140,38 +132,12 @@ const BlockInput: FC<IBlockInputProps> = ({ ...@@ -140,38 +132,12 @@ const BlockInput: FC<IBlockInputProps> = ({
</div>) </div>)
return ( return (
<div className={classNames('block-input w-full overflow-y-auto border-none rounded-lg')}> <div className={classNames('block-input w-full overflow-y-auto bg-white border-none rounded-xl')}>
{textAreaContent} {textAreaContent}
{/* footer */} {/* footer */}
{!readonly && ( {!readonly && (
<div className='flex item-center h-14 px-4'> <div className='pl-4 pb-2 flex'>
{isContentChanged
? (
<div className='flex items-center justify-between w-full'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div> <div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div>
<div className='flex space-x-2'>
<Button
onClick={handleCancel}
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleSubmit}
type="primary"
className='w-20 !h-8 !text-[13px]'
>
{t('common.operation.confirm')}
</Button>
</div>
</div>
)
: (
<p className="leading-5 text-xs text-gray-500">
{t('appDebug.promptTip')}
</p>
)}
</div> </div>
)} )}
......
...@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' ...@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import Tooltip from '../tooltip' import Tooltip from '../tooltip'
import TooltipPlus from '../tooltip-plus'
import copyStyle from './style.module.css' import copyStyle from './style.module.css'
type Props = { type Props = {
...@@ -54,3 +55,41 @@ const CopyFeedback = ({ content, selectorId, className }: Props) => { ...@@ -54,3 +55,41 @@ const CopyFeedback = ({ content, selectorId, className }: Props) => {
} }
export default CopyFeedback export default CopyFeedback
export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<TooltipPlus
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<div
className={`w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg ${
className ?? ''
}`}
onMouseLeave={onMouseLeave}
>
<div
onClick={onClickCopy}
className={`w-full h-full ${copyStyle.copyIcon} ${
isCopied ? copyStyle.copied : ''
}`}
></div>
</div>
</TooltipPlus>
)
}
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 15L12 20L17 15M7 9L12 4L17 9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9H16.5C18.9853 9 21 11.0147 21 13.5C21 15.9853 18.9853 18 16.5 18H12M3 9L7 5M3 9L7 13" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="variable">
<g id="Solid">
<path d="M13.8686 1.70487C13.7055 1.37481 13.3056 1.23952 12.9756 1.40268C12.6455 1.56585 12.5102 1.9657 12.6734 2.29576C13.5225 4.01329 14.0003 5.94969 14.0003 8.00031C14.0003 10.0509 13.5225 11.9873 12.6734 13.7049C12.5102 14.0349 12.6455 14.4348 12.9756 14.5979C13.3056 14.7611 13.7055 14.6258 13.8686 14.2958C14.8066 12.3984 15.3336 10.2602 15.3336 8.00031C15.3336 5.74041 14.8066 3.60221 13.8686 1.70487Z" fill="#2970FF"/>
<path d="M3.32724 2.29576C3.49041 1.9657 3.35511 1.56585 3.02506 1.40268C2.695 1.23952 2.29515 1.37481 2.13198 1.70487C1.19401 3.60221 0.666992 5.74041 0.666992 8.00031C0.666992 10.2602 1.19401 12.3984 2.13198 14.2958C2.29515 14.6258 2.695 14.7611 3.02506 14.5979C3.35511 14.4348 3.49041 14.0349 3.32724 13.7049C2.47815 11.9873 2.00033 10.0509 2.00033 8.00031C2.00033 5.94969 2.47815 4.01329 3.32724 2.29576Z" fill="#2970FF"/>
<path d="M9.33274 5.84142C9.74245 5.36093 10.3415 5.0835 10.973 5.0835H11.0328C11.4009 5.0835 11.6994 5.38197 11.6994 5.75016C11.6994 6.11835 11.4009 6.41683 11.0328 6.41683H10.973C10.7333 6.41683 10.5046 6.52209 10.3473 6.70653L8.78729 8.53612L9.28122 10.2739C9.29182 10.3112 9.32425 10.3335 9.35733 10.3335H10.2867C10.6549 10.3335 10.9534 10.632 10.9534 11.0002C10.9534 11.3684 10.6549 11.6668 10.2867 11.6668H9.35733C8.72419 11.6668 8.17111 11.2451 7.99868 10.6385L7.74768 9.75536L6.7641 10.9089C6.35439 11.3894 5.75537 11.6668 5.12387 11.6668H5.06409C4.6959 11.6668 4.39742 11.3684 4.39742 11.0002C4.39742 10.632 4.6959 10.3335 5.06409 10.3335H5.12387C5.36357 10.3335 5.59225 10.2282 5.74952 10.0438L7.30963 8.21412L6.81573 6.47639C6.80513 6.43909 6.7727 6.41683 6.73962 6.41683H5.81022C5.44203 6.41683 5.14355 6.11835 5.14355 5.75016C5.14355 5.38197 5.44203 5.0835 5.81022 5.0835H6.73962C7.37276 5.0835 7.92584 5.5052 8.09826 6.11186L8.34924 6.99487L9.33274 5.84142Z" fill="#2970FF"/>
</g>
</g>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9 15L11 17L15.5 12.5M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path id="Icon_2" d="M9.33366 7.3335H5.33366M6.66699 10.0002H5.33366M10.667 4.66683H5.33366M13.3337 4.5335V11.4668C13.3337 12.5869 13.3337 13.147 13.1157 13.5748C12.9239 13.9511 12.618 14.2571 12.2416 14.4488C11.8138 14.6668 11.2538 14.6668 10.1337 14.6668H5.86699C4.74689 14.6668 4.18683 14.6668 3.75901 14.4488C3.38269 14.2571 3.07673 13.9511 2.88498 13.5748C2.66699 13.147 2.66699 12.5869 2.66699 11.4668V4.5335C2.66699 3.41339 2.66699 2.85334 2.88498 2.42552C3.07673 2.04919 3.38269 1.74323 3.75901 1.55148C4.18683 1.3335 4.74689 1.3335 5.86699 1.3335H10.1337C11.2538 1.3335 11.8138 1.3335 12.2416 1.55148C12.618 1.74323 12.9239 2.04919 13.1157 2.42552C13.3337 2.85334 13.3337 3.41339 13.3337 4.5335Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Left Icon">
<path id="Icon" d="M1.75 4.6665L8.75 4.6665M8.75 4.6665C8.75 5.633 9.5335 6.4165 10.5 6.4165C11.4665 6.4165 12.25 5.633 12.25 4.6665C12.25 3.70001 11.4665 2.9165 10.5 2.9165C9.5335 2.9165 8.75 3.70001 8.75 4.6665ZM5.25 9.33317L12.25 9.33317M5.25 9.33317C5.25 10.2997 4.4665 11.0832 3.5 11.0832C2.5335 11.0832 1.75 10.2997 1.75 9.33317C1.75 8.36667 2.5335 7.58317 3.5 7.58317C4.4665 7.58317 5.25 8.36667 5.25 9.33317Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5H9M9 5C9 6.10457 9.89543 7 11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5ZM17 5L21 5M3 12H9M17 12H21M17 12C17 10.8954 16.1046 10 15 10C13.8954 10 13 10.8954 13 12C13 13.1046 13.8954 14 15 14C16.1046 14 17 13.1046 17 12ZM3 19H7M7 19C7 20.1046 7.89543 21 9 21C10.1046 21 11 20.1046 11 19C11 17.8954 10.1046 17 9 17C7.89543 17 7 17.8954 7 19ZM15 19H21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="file-05">
<g id="Solid">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.66667 1.34356C8.66667 1.32602 8.66667 1.31725 8.66591 1.30135C8.65018 0.972168 8.3607 0.682824 8.03151 0.667251C8.01562 0.666499 8.0104 0.666501 8.00001 0.666504H5.8391C5.30248 0.666497 4.85957 0.666491 4.49878 0.695968C4.12405 0.726585 3.77958 0.792295 3.45603 0.957155C2.95426 1.21282 2.54631 1.62077 2.29065 2.12253C2.12579 2.44609 2.06008 2.79056 2.02946 3.16529C1.99999 3.52608 1.99999 3.96899 2 4.50562V11.494C1.99999 12.0307 1.99999 12.4736 2.02946 12.8344C2.06008 13.2091 2.12579 13.5536 2.29065 13.8771C2.54631 14.3789 2.95426 14.7869 3.45603 15.0425C3.77958 15.2074 4.12405 15.2731 4.49878 15.3037C4.85958 15.3332 5.30248 15.3332 5.83912 15.3332H10.1609C10.6975 15.3332 11.1404 15.3332 11.5012 15.3037C11.8759 15.2731 12.2204 15.2074 12.544 15.0425C13.0457 14.7869 13.4537 14.3789 13.7093 13.8771C13.8742 13.5536 13.9399 13.2091 13.9705 12.8344C14 12.4736 14 12.0307 14 11.4941V6.66646C14 6.65611 14 6.65093 13.9993 6.63505C13.9837 6.30583 13.6943 6.01631 13.3651 6.0006C13.3492 5.99985 13.3405 5.99985 13.323 5.99985L10.3787 5.99985C10.2105 5.99987 10.0466 5.99989 9.90785 5.98855C9.75545 5.9761 9.57563 5.94672 9.39468 5.85452C9.1438 5.72669 8.93983 5.52272 8.81199 5.27183C8.7198 5.09088 8.69042 4.91106 8.67797 4.75867C8.66663 4.61989 8.66665 4.45603 8.66667 4.28778L8.66667 1.34356ZM5.33333 8.6665C4.96514 8.6665 4.66667 8.96498 4.66667 9.33317C4.66667 9.70136 4.96514 9.99984 5.33333 9.99984H10.6667C11.0349 9.99984 11.3333 9.70136 11.3333 9.33317C11.3333 8.96498 11.0349 8.6665 10.6667 8.6665H5.33333ZM5.33333 11.3332C4.96514 11.3332 4.66667 11.6316 4.66667 11.9998C4.66667 12.368 4.96514 12.6665 5.33333 12.6665H9.33333C9.70152 12.6665 10 12.368 10 11.9998C10 11.6316 9.70152 11.3332 9.33333 11.3332H5.33333Z" fill="#6938EF"/>
<path d="M12.6053 4.6665C12.8011 4.6665 12.8989 4.6665 12.9791 4.61735C13.0923 4.54794 13.16 4.3844 13.129 4.25526C13.107 4.16382 13.0432 4.10006 12.9155 3.97253L10.694 1.75098C10.5664 1.62333 10.5027 1.5595 10.4112 1.53752C10.2821 1.50648 10.1186 1.57417 10.0492 1.6874C10 1.76757 10 1.86545 10 2.0612L10 4.13315C10 4.31982 10 4.41316 10.0363 4.48446C10.0683 4.54718 10.1193 4.59818 10.182 4.63014C10.2533 4.66647 10.3466 4.66647 10.5333 4.66647L12.6053 4.6665Z" fill="#6938EF"/>
</g>
</g>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M0.666993 4.10794C0.666981 3.75652 0.666972 3.45333 0.687374 3.20362C0.708908 2.94006 0.756452 2.67791 0.884981 2.42566C1.07673 2.04933 1.38269 1.74337 1.75901 1.55163C2.01127 1.4231 2.27341 1.37555 2.53698 1.35402C2.78669 1.33362 3.08986 1.33363 3.4413 1.33364L6.0981 1.33357C6.4938 1.33304 6.84179 1.33258 7.16176 1.44295C7.44201 1.53961 7.69726 1.69737 7.90905 1.9048C8.15086 2.14164 8.30607 2.45309 8.48257 2.80725L9.07895 4.00016H11.4945C12.0312 4.00015 12.4741 4.00015 12.8349 4.02963C13.2096 4.06024 13.5541 4.12595 13.8776 4.29081C14.3794 4.54648 14.7873 4.95442 15.043 5.45619C15.2079 5.77975 15.2736 6.12421 15.3042 6.49895C15.3337 6.85974 15.3337 7.30264 15.3337 7.83928V10.8277C15.3337 11.3644 15.3337 11.8073 15.3042 12.168C15.2736 12.5428 15.2079 12.8872 15.043 13.2108C14.7873 13.7126 14.3794 14.1205 13.8776 14.3762C13.5541 14.541 13.2096 14.6068 12.8349 14.6374C12.4741 14.6668 12.0312 14.6668 11.4945 14.6668H4.50614C3.9695 14.6668 3.52657 14.6668 3.16578 14.6374C2.79104 14.6068 2.44658 14.541 2.12302 14.3762C1.62125 14.1205 1.2133 13.7126 0.957643 13.2108C0.792782 12.8872 0.727073 12.5428 0.696456 12.168C0.666978 11.8073 0.666985 11.3643 0.666993 10.8277V4.10794ZM6.01519 2.66697C6.54213 2.66697 6.64658 2.67567 6.727 2.70341C6.82041 2.73563 6.9055 2.78822 6.97609 2.85736C7.03687 2.91688 7.09136 3.00642 7.32701 3.47773L7.58823 4.00016L2.00038 4.00016C2.00067 3.69017 2.00271 3.47827 2.01628 3.3122C2.03108 3.13109 2.05619 3.06394 2.07299 3.03098C2.13691 2.90554 2.23889 2.80355 2.36433 2.73964C2.3973 2.72284 2.46444 2.69772 2.64555 2.68292C2.83444 2.66749 3.08263 2.66697 3.46699 2.66697H6.01519Z" fill="#444CE7"/>
</g>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="message-clock-circle">
<path id="Solid" d="M1.33301 8.00016C1.33301 4.31826 4.31778 1.3335 7.99967 1.3335C11.6816 1.3335 14.6663 4.31826 14.6663 8.00016C14.6663 11.6821 11.6816 14.6668 7.99967 14.6668C7.11413 14.6668 6.26734 14.4938 5.49248 14.1791C5.42249 14.1507 5.38209 14.1344 5.35225 14.1231L5.34304 14.1197L5.33987 14.1202C5.31527 14.1235 5.28173 14.129 5.21771 14.1397L2.82667 14.5382C2.71958 14.5561 2.59976 14.5761 2.4957 14.5839C2.38225 14.5925 2.20175 14.5955 2.01101 14.5137C1.77521 14.4125 1.5873 14.2246 1.48616 13.9888C1.40435 13.7981 1.40733 13.6176 1.41589 13.5041C1.42375 13.4001 1.44375 13.2803 1.46163 13.1732L1.86015 10.7821C1.87082 10.7181 1.87634 10.6846 1.87967 10.66L1.8801 10.6568L1.87669 10.6476C1.86549 10.6178 1.84914 10.5773 1.82071 10.5074C1.50602 9.7325 1.33301 8.88571 1.33301 8.00016ZM7.99967 5.3335C7.99967 4.96531 7.7012 4.66683 7.33301 4.66683C6.96482 4.66683 6.66634 4.96531 6.66634 5.3335V8.66683C6.66634 9.03502 6.96482 9.3335 7.33301 9.3335H10.6663C11.0345 9.3335 11.333 9.03502 11.333 8.66683C11.333 8.29864 11.0345 8.00016 10.6663 8.00016H7.99967V5.3335Z" fill="#DD2590"/>
</g>
</svg>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="user-edit 2" clip-path="url(#clip0_10419_49994)">
<g id="Group">
<path id="Vector" d="M5.83333 6.41667C7.60525 6.41667 9.04167 4.98025 9.04167 3.20833C9.04167 1.43642 7.60525 0 5.83333 0C4.06142 0 2.625 1.43642 2.625 3.20833C2.625 4.98025 4.06142 6.41667 5.83333 6.41667Z" fill="#FD853A"/>
<path id="Vector_2" d="M5.90917 13.2465L6.78417 10.6221C6.85533 10.4086 6.97725 10.2114 7.1365 10.0522L8.79083 8.39783C7.92225 7.88391 6.91308 7.5835 5.83333 7.5835C2.61683 7.5835 0 10.2003 0 13.4168C0 13.7394 0.261333 14.0002 0.583333 14.0002H5.86717C5.817 13.7546 5.82575 13.4962 5.90917 13.2465Z" fill="#FD853A"/>
<path id="Vector_3" d="M13.5524 7.44766C12.9562 6.85208 11.9856 6.85208 11.39 7.44766L7.96057 10.8771C7.92849 10.9092 7.90457 10.9482 7.88999 10.9908L7.01499 13.6158C6.97999 13.7208 7.0074 13.8363 7.08557 13.9145C7.14099 13.9705 7.21565 13.9997 7.29207 13.9997C7.32299 13.9997 7.3539 13.9944 7.38424 13.9851L10.0092 13.1101C10.0524 13.0961 10.0915 13.0716 10.123 13.0395L13.5524 9.61008C14.148 9.0145 14.148 8.04383 13.5524 7.44766Z" fill="#FD853A"/>
</g>
</g>
<defs>
<clipPath id="clip0_10419_49994">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M7 15L12 20L17 15M7 9L12 4L17 9",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "ChevronSelectorVertical"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ChevronSelectorVertical.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ChevronSelectorVertical'
export default Icon
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3 9H16.5C18.9853 9 21 11.0147 21 13.5C21 15.9853 18.9853 18 16.5 18H12M3 9L7 5M3 9L7 13",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "FlipBackward"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './FlipBackward.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'FlipBackward'
export default Icon
...@@ -3,5 +3,7 @@ export { default as ArrowUpRight } from './ArrowUpRight' ...@@ -3,5 +3,7 @@ export { default as ArrowUpRight } from './ArrowUpRight'
export { default as ChevronDownDouble } from './ChevronDownDouble' export { default as ChevronDownDouble } from './ChevronDownDouble'
export { default as ChevronDown } from './ChevronDown' export { default as ChevronDown } from './ChevronDown'
export { default as ChevronRight } from './ChevronRight' export { default as ChevronRight } from './ChevronRight'
export { default as ChevronSelectorVertical } from './ChevronSelectorVertical'
export { default as FlipBackward } from './FlipBackward'
export { default as RefreshCcw01 } from './RefreshCcw01' export { default as RefreshCcw01 } from './RefreshCcw01'
export { default as RefreshCw05 } from './RefreshCw05' export { default as RefreshCw05 } from './RefreshCw05'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "variable"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Solid"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.8686 1.70487C13.7055 1.37481 13.3056 1.23952 12.9756 1.40268C12.6455 1.56585 12.5102 1.9657 12.6734 2.29576C13.5225 4.01329 14.0003 5.94969 14.0003 8.00031C14.0003 10.0509 13.5225 11.9873 12.6734 13.7049C12.5102 14.0349 12.6455 14.4348 12.9756 14.5979C13.3056 14.7611 13.7055 14.6258 13.8686 14.2958C14.8066 12.3984 15.3336 10.2602 15.3336 8.00031C15.3336 5.74041 14.8066 3.60221 13.8686 1.70487Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.32724 2.29576C3.49041 1.9657 3.35511 1.56585 3.02506 1.40268C2.695 1.23952 2.29515 1.37481 2.13198 1.70487C1.19401 3.60221 0.666992 5.74041 0.666992 8.00031C0.666992 10.2602 1.19401 12.3984 2.13198 14.2958C2.29515 14.6258 2.695 14.7611 3.02506 14.5979C3.35511 14.4348 3.49041 14.0349 3.32724 13.7049C2.47815 11.9873 2.00033 10.0509 2.00033 8.00031C2.00033 5.94969 2.47815 4.01329 3.32724 2.29576Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.33274 5.84142C9.74245 5.36093 10.3415 5.0835 10.973 5.0835H11.0328C11.4009 5.0835 11.6994 5.38197 11.6994 5.75016C11.6994 6.11835 11.4009 6.41683 11.0328 6.41683H10.973C10.7333 6.41683 10.5046 6.52209 10.3473 6.70653L8.78729 8.53612L9.28122 10.2739C9.29182 10.3112 9.32425 10.3335 9.35733 10.3335H10.2867C10.6549 10.3335 10.9534 10.632 10.9534 11.0002C10.9534 11.3684 10.6549 11.6668 10.2867 11.6668H9.35733C8.72419 11.6668 8.17111 11.2451 7.99868 10.6385L7.74768 9.75536L6.7641 10.9089C6.35439 11.3894 5.75537 11.6668 5.12387 11.6668H5.06409C4.6959 11.6668 4.39742 11.3684 4.39742 11.0002C4.39742 10.632 4.6959 10.3335 5.06409 10.3335H5.12387C5.36357 10.3335 5.59225 10.2282 5.74952 10.0438L7.30963 8.21412L6.81573 6.47639C6.80513 6.43909 6.7727 6.41683 6.73962 6.41683H5.81022C5.44203 6.41683 5.14355 6.11835 5.14355 5.75016C5.14355 5.38197 5.44203 5.0835 5.81022 5.0835H6.73962C7.37276 5.0835 7.92584 5.5052 8.09826 6.11186L8.34924 6.99487L9.33274 5.84142Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Variable"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Variable.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Variable'
export default Icon
...@@ -3,3 +3,4 @@ export { default as Container } from './Container' ...@@ -3,3 +3,4 @@ export { default as Container } from './Container'
export { default as Database01 } from './Database01' export { default as Database01 } from './Database01'
export { default as Database03 } from './Database03' export { default as Database03 } from './Database03'
export { default as PuzzlePiece01 } from './PuzzlePiece01' export { default as PuzzlePiece01 } from './PuzzlePiece01'
export { default as Variable } from './Variable'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9 15L11 17L15.5 12.5M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "ClipboardCheck"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ClipboardCheck.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ClipboardCheck'
export default Icon
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon_2",
"d": "M9.33366 7.3335H5.33366M6.66699 10.0002H5.33366M10.667 4.66683H5.33366M13.3337 4.5335V11.4668C13.3337 12.5869 13.3337 13.147 13.1157 13.5748C12.9239 13.9511 12.618 14.2571 12.2416 14.4488C11.8138 14.6668 11.2538 14.6668 10.1337 14.6668H5.86699C4.74689 14.6668 4.18683 14.6668 3.75901 14.4488C3.38269 14.2571 3.07673 13.9511 2.88498 13.5748C2.66699 13.147 2.66699 12.5869 2.66699 11.4668V4.5335C2.66699 3.41339 2.66699 2.85334 2.88498 2.42552C3.07673 2.04919 3.38269 1.74323 3.75901 1.55148C4.18683 1.3335 4.74689 1.3335 5.86699 1.3335H10.1337C11.2538 1.3335 11.8138 1.3335 12.2416 1.55148C12.618 1.74323 12.9239 2.04919 13.1157 2.42552C13.3337 2.85334 13.3337 3.41339 13.3337 4.5335Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "File02"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './File02.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'File02'
export default Icon
export { default as ClipboardCheck } from './ClipboardCheck'
export { default as Clipboard } from './Clipboard' export { default as Clipboard } from './Clipboard'
export { default as File02 } from './File02'
export { default as FilePlus02 } from './FilePlus02' export { default as FilePlus02 } from './FilePlus02'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Left Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M1.75 4.6665L8.75 4.6665M8.75 4.6665C8.75 5.633 9.5335 6.4165 10.5 6.4165C11.4665 6.4165 12.25 5.633 12.25 4.6665C12.25 3.70001 11.4665 2.9165 10.5 2.9165C9.5335 2.9165 8.75 3.70001 8.75 4.6665ZM5.25 9.33317L12.25 9.33317M5.25 9.33317C5.25 10.2997 4.4665 11.0832 3.5 11.0832C2.5335 11.0832 1.75 10.2997 1.75 9.33317C1.75 8.36667 2.5335 7.58317 3.5 7.58317C4.4665 7.58317 5.25 8.36667 5.25 9.33317Z",
"stroke": "currentColor",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "Settings04"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Settings04.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Settings04'
export default Icon
...@@ -13,6 +13,7 @@ export { default as LogOut01 } from './LogOut01' ...@@ -13,6 +13,7 @@ export { default as LogOut01 } from './LogOut01'
export { default as Pin02 } from './Pin02' export { default as Pin02 } from './Pin02'
export { default as Plus } from './Plus' export { default as Plus } from './Plus'
export { default as SearchLg } from './SearchLg' export { default as SearchLg } from './SearchLg'
export { default as Settings04 } from './Settings04'
export { default as Target04 } from './Target04' export { default as Target04 } from './Target04'
export { default as Trash03 } from './Trash03' export { default as Trash03 } from './Trash03'
export { default as XClose } from './XClose' export { default as XClose } from './XClose'
......
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3 5H9M9 5C9 6.10457 9.89543 7 11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5ZM17 5L21 5M3 12H9M17 12H21M17 12C17 10.8954 16.1046 10 15 10C13.8954 10 13 10.8954 13 12C13 13.1046 13.8954 14 15 14C16.1046 14 17 13.1046 17 12ZM3 19H7M7 19C7 20.1046 7.89543 21 9 21C10.1046 21 11 20.1046 11 19C11 17.8954 10.1046 17 9 17C7.89543 17 7 17.8954 7 19ZM15 19H21",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "SlidersH"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './SlidersH.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'SlidersH'
export default Icon
export { default as Microphone01 } from './Microphone01' export { default as Microphone01 } from './Microphone01'
export { default as SlidersH } from './SlidersH'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "file-05"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Solid"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8.66667 1.34356C8.66667 1.32602 8.66667 1.31725 8.66591 1.30135C8.65018 0.972168 8.3607 0.682824 8.03151 0.667251C8.01562 0.666499 8.0104 0.666501 8.00001 0.666504H5.8391C5.30248 0.666497 4.85957 0.666491 4.49878 0.695968C4.12405 0.726585 3.77958 0.792295 3.45603 0.957155C2.95426 1.21282 2.54631 1.62077 2.29065 2.12253C2.12579 2.44609 2.06008 2.79056 2.02946 3.16529C1.99999 3.52608 1.99999 3.96899 2 4.50562V11.494C1.99999 12.0307 1.99999 12.4736 2.02946 12.8344C2.06008 13.2091 2.12579 13.5536 2.29065 13.8771C2.54631 14.3789 2.95426 14.7869 3.45603 15.0425C3.77958 15.2074 4.12405 15.2731 4.49878 15.3037C4.85958 15.3332 5.30248 15.3332 5.83912 15.3332H10.1609C10.6975 15.3332 11.1404 15.3332 11.5012 15.3037C11.8759 15.2731 12.2204 15.2074 12.544 15.0425C13.0457 14.7869 13.4537 14.3789 13.7093 13.8771C13.8742 13.5536 13.9399 13.2091 13.9705 12.8344C14 12.4736 14 12.0307 14 11.4941V6.66646C14 6.65611 14 6.65093 13.9993 6.63505C13.9837 6.30583 13.6943 6.01631 13.3651 6.0006C13.3492 5.99985 13.3405 5.99985 13.323 5.99985L10.3787 5.99985C10.2105 5.99987 10.0466 5.99989 9.90785 5.98855C9.75545 5.9761 9.57563 5.94672 9.39468 5.85452C9.1438 5.72669 8.93983 5.52272 8.81199 5.27183C8.7198 5.09088 8.69042 4.91106 8.67797 4.75867C8.66663 4.61989 8.66665 4.45603 8.66667 4.28778L8.66667 1.34356ZM5.33333 8.6665C4.96514 8.6665 4.66667 8.96498 4.66667 9.33317C4.66667 9.70136 4.96514 9.99984 5.33333 9.99984H10.6667C11.0349 9.99984 11.3333 9.70136 11.3333 9.33317C11.3333 8.96498 11.0349 8.6665 10.6667 8.6665H5.33333ZM5.33333 11.3332C4.96514 11.3332 4.66667 11.6316 4.66667 11.9998C4.66667 12.368 4.96514 12.6665 5.33333 12.6665H9.33333C9.70152 12.6665 10 12.368 10 11.9998C10 11.6316 9.70152 11.3332 9.33333 11.3332H5.33333Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M12.6053 4.6665C12.8011 4.6665 12.8989 4.6665 12.9791 4.61735C13.0923 4.54794 13.16 4.3844 13.129 4.25526C13.107 4.16382 13.0432 4.10006 12.9155 3.97253L10.694 1.75098C10.5664 1.62333 10.5027 1.5595 10.4112 1.53752C10.2821 1.50648 10.1186 1.57417 10.0492 1.6874C10 1.76757 10 1.86545 10 2.0612L10 4.13315C10 4.31982 10 4.41316 10.0363 4.48446C10.0683 4.54718 10.1193 4.59818 10.182 4.63014C10.2533 4.66647 10.3466 4.66647 10.5333 4.66647L12.6053 4.6665Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "File05"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './File05.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'File05'
export default Icon
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M0.666993 4.10794C0.666981 3.75652 0.666972 3.45333 0.687374 3.20362C0.708908 2.94006 0.756452 2.67791 0.884981 2.42566C1.07673 2.04933 1.38269 1.74337 1.75901 1.55163C2.01127 1.4231 2.27341 1.37555 2.53698 1.35402C2.78669 1.33362 3.08986 1.33363 3.4413 1.33364L6.0981 1.33357C6.4938 1.33304 6.84179 1.33258 7.16176 1.44295C7.44201 1.53961 7.69726 1.69737 7.90905 1.9048C8.15086 2.14164 8.30607 2.45309 8.48257 2.80725L9.07895 4.00016H11.4945C12.0312 4.00015 12.4741 4.00015 12.8349 4.02963C13.2096 4.06024 13.5541 4.12595 13.8776 4.29081C14.3794 4.54648 14.7873 4.95442 15.043 5.45619C15.2079 5.77975 15.2736 6.12421 15.3042 6.49895C15.3337 6.85974 15.3337 7.30264 15.3337 7.83928V10.8277C15.3337 11.3644 15.3337 11.8073 15.3042 12.168C15.2736 12.5428 15.2079 12.8872 15.043 13.2108C14.7873 13.7126 14.3794 14.1205 13.8776 14.3762C13.5541 14.541 13.2096 14.6068 12.8349 14.6374C12.4741 14.6668 12.0312 14.6668 11.4945 14.6668H4.50614C3.9695 14.6668 3.52657 14.6668 3.16578 14.6374C2.79104 14.6068 2.44658 14.541 2.12302 14.3762C1.62125 14.1205 1.2133 13.7126 0.957643 13.2108C0.792782 12.8872 0.727073 12.5428 0.696456 12.168C0.666978 11.8073 0.666985 11.3643 0.666993 10.8277V4.10794ZM6.01519 2.66697C6.54213 2.66697 6.64658 2.67567 6.727 2.70341C6.82041 2.73563 6.9055 2.78822 6.97609 2.85736C7.03687 2.91688 7.09136 3.00642 7.32701 3.47773L7.58823 4.00016L2.00038 4.00016C2.00067 3.69017 2.00271 3.47827 2.01628 3.3122C2.03108 3.13109 2.05619 3.06394 2.07299 3.03098C2.13691 2.90554 2.23889 2.80355 2.36433 2.73964C2.3973 2.72284 2.46444 2.69772 2.64555 2.68292C2.83444 2.66749 3.08263 2.66697 3.46699 2.66697H6.01519Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "Folder"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Folder.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Folder'
export default Icon
export { default as File05 } from './File05'
export { default as Folder } from './Folder'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "message-clock-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"d": "M1.33301 8.00016C1.33301 4.31826 4.31778 1.3335 7.99967 1.3335C11.6816 1.3335 14.6663 4.31826 14.6663 8.00016C14.6663 11.6821 11.6816 14.6668 7.99967 14.6668C7.11413 14.6668 6.26734 14.4938 5.49248 14.1791C5.42249 14.1507 5.38209 14.1344 5.35225 14.1231L5.34304 14.1197L5.33987 14.1202C5.31527 14.1235 5.28173 14.129 5.21771 14.1397L2.82667 14.5382C2.71958 14.5561 2.59976 14.5761 2.4957 14.5839C2.38225 14.5925 2.20175 14.5955 2.01101 14.5137C1.77521 14.4125 1.5873 14.2246 1.48616 13.9888C1.40435 13.7981 1.40733 13.6176 1.41589 13.5041C1.42375 13.4001 1.44375 13.2803 1.46163 13.1732L1.86015 10.7821C1.87082 10.7181 1.87634 10.6846 1.87967 10.66L1.8801 10.6568L1.87669 10.6476C1.86549 10.6178 1.84914 10.5773 1.82071 10.5074C1.50602 9.7325 1.33301 8.88571 1.33301 8.00016ZM7.99967 5.3335C7.99967 4.96531 7.7012 4.66683 7.33301 4.66683C6.96482 4.66683 6.66634 4.96531 6.66634 5.3335V8.66683C6.66634 9.03502 6.96482 9.3335 7.33301 9.3335H10.6663C11.0345 9.3335 11.333 9.03502 11.333 8.66683C11.333 8.29864 11.0345 8.00016 10.6663 8.00016H7.99967V5.3335Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "MessageClockCircle"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './MessageClockCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'MessageClockCircle'
export default Icon
export { default as CheckCircle } from './CheckCircle' export { default as CheckCircle } from './CheckCircle'
export { default as CheckDone01 } from './CheckDone01' export { default as CheckDone01 } from './CheckDone01'
export { default as Download02 } from './Download02' export { default as Download02 } from './Download02'
export { default as MessageClockCircle } from './MessageClockCircle'
export { default as Target04 } from './Target04' export { default as Target04 } from './Target04'
export { default as XCircle } from './XCircle' export { default as XCircle } from './XCircle'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "user-edit 2",
"clip-path": "url(#clip0_10419_49994)"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Group"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M5.83333 6.41667C7.60525 6.41667 9.04167 4.98025 9.04167 3.20833C9.04167 1.43642 7.60525 0 5.83333 0C4.06142 0 2.625 1.43642 2.625 3.20833C2.625 4.98025 4.06142 6.41667 5.83333 6.41667Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_2",
"d": "M5.90917 13.2465L6.78417 10.6221C6.85533 10.4086 6.97725 10.2114 7.1365 10.0522L8.79083 8.39783C7.92225 7.88391 6.91308 7.5835 5.83333 7.5835C2.61683 7.5835 0 10.2003 0 13.4168C0 13.7394 0.261333 14.0002 0.583333 14.0002H5.86717C5.817 13.7546 5.82575 13.4962 5.90917 13.2465Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_3",
"d": "M13.5524 7.44766C12.9562 6.85208 11.9856 6.85208 11.39 7.44766L7.96057 10.8771C7.92849 10.9092 7.90457 10.9482 7.88999 10.9908L7.01499 13.6158C6.97999 13.7208 7.0074 13.8363 7.08557 13.9145C7.14099 13.9705 7.21565 13.9997 7.29207 13.9997C7.32299 13.9997 7.3539 13.9944 7.38424 13.9851L10.0092 13.1101C10.0524 13.0961 10.0915 13.0716 10.123 13.0395L13.5524 9.61008C14.148 9.0145 14.148 8.04383 13.5524 7.44766Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_10419_49994"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "14",
"height": "14",
"fill": "white"
},
"children": []
}
]
}
]
}
]
},
"name": "UserEdit02"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './UserEdit02.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'UserEdit02'
export default Icon
export { default as User01 } from './User01' export { default as User01 } from './User01'
export { default as UserEdit02 } from './UserEdit02'
export { default as Users01 } from './Users01' export { default as Users01 } from './Users01'
export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'
export const checkHasContextBlock = (text: string) => {
if (!text)
return false
return text.includes(CONTEXT_PLACEHOLDER_TEXT)
}
export const checkHasHistoryBlock = (text: string) => {
if (!text)
return false
return text.includes(HISTORY_PLACEHOLDER_TEXT)
}
export const checkHasQueryBlock = (text: string) => {
if (!text)
return false
return text.includes(QUERY_PLACEHOLDER_TEXT)
}
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import type { Dispatch, RefObject, SetStateAction } from 'react'
import type {
Klass,
LexicalCommand,
LexicalEditor,
TextNode,
} from 'lexical'
import {
$getNodeByKey,
$getSelection,
$isDecoratorNode,
$isNodeSelection,
COMMAND_PRIORITY_LOW,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
} from 'lexical'
import type { EntityMatch } from '@lexical/text'
import {
mergeRegister,
} from '@lexical/utils'
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $isContextBlockNode } from './plugins/context-block/node'
import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block'
import { $isHistoryBlockNode } from './plugins/history-block/node'
import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block'
import { $isQueryBlockNode } from './plugins/query-block/node'
import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
import type { CustomTextNode } from './plugins/custom-text/node'
import { registerLexicalTextEntity } from './utils'
export type UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
export const useSelectOrDelete: UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => {
const ref = useRef<HTMLDivElement>(null)
const [editor] = useLexicalComposerContext()
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
const handleDelete = useCallback(
(event: KeyboardEvent) => {
const selection = $getSelection()
const nodes = selection?.getNodes()
if (
!isSelected
&& nodes?.length === 1
&& (
($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND)
|| ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND)
|| ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
)
)
editor.dispatchCommand(command, undefined)
if (isSelected && $isNodeSelection(selection)) {
event.preventDefault()
const node = $getNodeByKey(nodeKey)
if ($isDecoratorNode(node)) {
if (command)
editor.dispatchCommand(command, undefined)
node.remove()
}
}
return false
},
[isSelected, nodeKey, command, editor],
)
const handleSelect = useCallback((e: MouseEvent) => {
e.stopPropagation()
clearSelection()
setSelected(true)
}, [setSelected, clearSelection])
useEffect(() => {
const ele = ref.current
if (ele)
ele.addEventListener('click', handleSelect)
return () => {
if (ele)
ele.removeEventListener('click', handleSelect)
}
}, [handleSelect])
useEffect(() => {
return mergeRegister(
editor.registerCommand(
KEY_DELETE_COMMAND,
handleDelete,
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
KEY_BACKSPACE_COMMAND,
handleDelete,
COMMAND_PRIORITY_LOW,
),
)
}, [editor, clearSelection, handleDelete])
return [ref, isSelected]
}
export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
export const useTrigger: UseTriggerHandler = () => {
const triggerRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const handleOpen = useCallback((e: MouseEvent) => {
e.stopPropagation()
setOpen(v => !v)
}, [])
useEffect(() => {
const trigger = triggerRef.current
if (trigger)
trigger.addEventListener('click', handleOpen)
return () => {
if (trigger)
trigger.removeEventListener('click', handleOpen)
}
}, [handleOpen])
return [triggerRef, open, setOpen]
}
export function useLexicalTextEntity<T extends TextNode>(
getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>,
createNode: (textNode: CustomTextNode) => T,
) {
const [editor] = useLexicalComposerContext()
useEffect(() => {
return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode))
}, [createNode, editor, getMatch, targetNode])
}
export type MenuTextMatch = {
leadOffset: number
matchingString: string
replaceableString: string
}
export type TriggerFn = (
text: string,
editor: LexicalEditor,
) => MenuTextMatch | null
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
export function useBasicTypeaheadTriggerMatch(
trigger: string,
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
): TriggerFn {
return useCallback(
(text: string) => {
const validChars = `[^${trigger}${PUNCTUATION}\\s]`
const TypeaheadTriggerRegex = new RegExp(
`([^${trigger}]|^)(`
+ `[${trigger}]`
+ `((?:${validChars}){0,${maxLength}})`
+ ')$',
)
const match = TypeaheadTriggerRegex.exec(text)
if (match !== null) {
const maybeLeadingWhitespace = match[1]
const matchingString = match[3]
if (matchingString.length >= minLength) {
return {
leadOffset: match.index + maybeLeadingWhitespace.length,
matchingString,
replaceableString: match[2],
}
}
}
return null
},
[maxLength, minLength, trigger],
)
}
'use client'
import type { FC } from 'react'
import { useEffect } from 'react'
import type {
EditorState,
} from 'lexical'
import {
$getRoot,
TextNode,
} from 'lexical'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
// import TreeView from './plugins/tree-view'
import Placeholder from './plugins/placeholder'
import ComponentPicker from './plugins/component-picker'
import VariablePicker from './plugins/variable-picker'
import ContextBlock from './plugins/context-block'
import { ContextBlockNode } from './plugins/context-block/node'
import ContextBlockReplacementBlock from './plugins/context-block-replacement-block'
import HistoryBlock from './plugins/history-block'
import { HistoryBlockNode } from './plugins/history-block/node'
import HistoryBlockReplacementBlock from './plugins/history-block-replacement-block'
import QueryBlock from './plugins/query-block'
import { QueryBlockNode } from './plugins/query-block/node'
import QueryBlockReplacementBlock from './plugins/query-block-replacement-block'
import VariableBlock from './plugins/variable-block'
import VariableValueBlock from './plugins/variable-value-block'
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
import { CustomTextNode } from './plugins/custom-text/node'
import OnBlurBlock from './plugins/on-blur-block'
import { textToEditorState } from './utils'
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
import type { Option } from './plugins/variable-picker'
import {
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
} from './constants'
import { useEventEmitterContextContext } from '@/context/event-emitter'
export type PromptEditorProps = {
className?: string
value?: string
editable?: boolean
onChange?: (text: string) => void
onBlur?: () => void
contextBlock?: {
selectable?: boolean
datasets: Dataset[]
onInsert?: () => void
onDelete?: () => void
onAddContext: () => void
}
variableBlock?: {
selectable?: boolean
variables: Option[]
}
historyBlock?: {
show?: boolean
selectable?: boolean
history: RoleName
onInsert?: () => void
onDelete?: () => void
onEditRole: () => void
}
queryBlock?: {
show?: boolean
selectable?: boolean
onInsert?: () => void
onDelete?: () => void
}
}
const PromptEditor: FC<PromptEditorProps> = ({
className,
value,
editable = true,
onChange,
onBlur,
contextBlock = {
selectable: true,
datasets: [],
onAddContext: () => {},
onInsert: () => {},
onDelete: () => {},
},
historyBlock = {
show: true,
selectable: true,
history: {
user: '',
assistant: '',
},
onEditRole: () => {},
onInsert: () => {},
onDelete: () => {},
},
variableBlock = {
variables: [],
},
queryBlock = {
show: true,
selectable: true,
onInsert: () => {},
onDelete: () => {},
},
}) => {
const { eventEmitter } = useEventEmitterContextContext()
const initialConfig = {
namespace: 'prompt-editor',
nodes: [
CustomTextNode,
{
replace: TextNode,
with: (node: TextNode) => new CustomTextNode(node.__text),
},
ContextBlockNode,
HistoryBlockNode,
QueryBlockNode,
VariableValueBlockNode,
],
editorState: value ? textToEditorState(value as string) : null,
onError: (error: Error) => {
throw error
},
}
const handleEditorChange = (editorState: EditorState) => {
const text = editorState.read(() => $getRoot().getTextContent())
if (onChange)
onChange(text.replaceAll('\n\n', '\n'))
}
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_DATASETS_EVENT_EMITTER,
payload: contextBlock.datasets,
} as any)
}, [eventEmitter, contextBlock.datasets])
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_HISTORY_EVENT_EMITTER,
payload: historyBlock.history,
} as any)
}, [eventEmitter, historyBlock.history])
return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative'>
<RichTextPlugin
contentEditable={<ContentEditable className={`${className} outline-none text-sm text-gray-700 leading-6`} />}
placeholder={<Placeholder />}
ErrorBoundary={LexicalErrorBoundary}
/>
<ComponentPicker
contextDisabled={!contextBlock.selectable}
historyDisabled={!historyBlock.selectable}
historyShow={historyBlock.show}
queryDisabled={!queryBlock.selectable}
queryShow={queryBlock.show}
/>
<VariablePicker items={variableBlock.variables} />
<ContextBlock
datasets={contextBlock.datasets}
onAddContext={contextBlock.onAddContext}
onInsert={contextBlock.onInsert}
onDelete={contextBlock.onDelete}
/>
<ContextBlockReplacementBlock
datasets={contextBlock.datasets}
onAddContext={contextBlock.onAddContext}
onInsert={contextBlock.onInsert}
/>
<VariableBlock />
{
historyBlock.show && (
<>
<HistoryBlock
roleName={historyBlock.history}
onEditRole={historyBlock.onEditRole}
onInsert={historyBlock.onInsert}
onDelete={historyBlock.onDelete}
/>
<HistoryBlockReplacementBlock
roleName={historyBlock.history}
onEditRole={historyBlock.onEditRole}
onInsert={historyBlock.onInsert}
/>
</>
)
}
{
queryBlock.show && (
<>
<QueryBlock
onInsert={queryBlock.onInsert}
onDelete={queryBlock.onDelete}
/>
<QueryBlockReplacementBlock />
</>
)
}
<VariableValueBlock />
<OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} />
{/* <TreeView /> */}
</div>
</LexicalComposer>
)
}
export default PromptEditor
import type { FC } from 'react'
import { useCallback } from 'react'
import ReactDOM from 'react-dom'
import { useTranslation } from 'react-i18next'
import type { TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
} from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useBasicTypeaheadTriggerMatch } from '../hooks'
import { INSERT_CONTEXT_BLOCK_COMMAND } from './context-block'
import { INSERT_VARIABLE_BLOCK_COMMAND } from './variable-block'
import { INSERT_HISTORY_BLOCK_COMMAND } from './history-block'
import { INSERT_QUERY_BLOCK_COMMAND } from './query-block'
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
import { Variable } from '@/app/components/base/icons/src/vender/line/development'
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
class ComponentPickerOption extends MenuOption {
title: string
icon?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
desc: string
onSelect: (queryString: string) => void
disabled?: boolean
constructor(
title: string,
options: {
icon?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
desc: string
onSelect: (queryString: string) => void
disabled?: boolean
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.keyboardShortcut = options.keyboardShortcut
this.desc = options.desc
this.onSelect = options.onSelect.bind(this)
this.disabled = options.disabled
}
}
type ComponentPickerMenuItemProps = {
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: ComponentPickerOption
}
const ComponentPickerMenuItem: FC<ComponentPickerMenuItemProps> = ({
isSelected,
onClick,
onMouseEnter,
option,
}) => {
const { t } = useTranslation()
return (
<div
key={option.key}
className={`
flex items-center px-3 py-1.5 rounded-lg
${isSelected && !option.disabled && '!bg-gray-50'}
${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<div className='flex items-center justify-center mr-2 w-8 h-8 rounded-lg border border-gray-100'>
{option.icon}
</div>
<div className='grow'>
<div className='flex items-center justify-between h-5 text-sm text-gray-900'>
{option.title}
<span className='text-xs text-gray-400'>{option.disabled && t('common.promptEditor.existed')}</span>
</div>
<div className='text-xs text-gray-500'>{option.desc}</div>
</div>
</div>
)
}
type ComponentPickerProps = {
contextDisabled?: boolean
historyDisabled?: boolean
queryDisabled?: boolean
historyShow?: boolean
queryShow?: boolean
}
const ComponentPicker: FC<ComponentPickerProps> = ({
contextDisabled,
historyDisabled,
queryDisabled,
historyShow,
queryShow,
}) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0,
maxLength: 0,
})
const options = [
new ComponentPickerOption(t('common.promptEditor.context.item.title'), {
desc: t('common.promptEditor.context.item.desc'),
icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
onSelect: () => {
if (contextDisabled)
return
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
},
disabled: contextDisabled,
}),
new ComponentPickerOption(t('common.promptEditor.variable.item.title'), {
desc: t('common.promptEditor.variable.item.desc'),
icon: <Variable className='w-4 h-4 text-[#2970FF]' />,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_BLOCK_COMMAND, undefined)
},
}),
...historyShow
? [
new ComponentPickerOption(t('common.promptEditor.history.item.title'), {
desc: t('common.promptEditor.history.item.desc'),
icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
onSelect: () => {
if (historyDisabled)
return
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
},
disabled: historyDisabled,
}),
]
: [],
...queryShow
? [
new ComponentPickerOption(t('common.promptEditor.query.item.title'), {
desc: t('common.promptEditor.query.item.desc'),
icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
onSelect: () => {
if (queryDisabled)
return
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
},
disabled: queryDisabled,
}),
]
: [],
]
const onSelectOption = useCallback(
(
selectedOption: ComponentPickerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
matchingString: string,
) => {
editor.update(() => {
if (nodeToRemove)
nodeToRemove.remove()
selectedOption.onSelect(matchingString)
closeMenu()
})
},
[editor],
)
return (
<LexicalTypeaheadMenuPlugin
options={options}
onQueryChange={() => {}}
onSelectOption={onSelectOption}
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
) =>
(anchorElementRef.current && options.length)
? ReactDOM.createPortal(
<div className='mt-[25px] p-1 w-[400px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
{options.map((option, i: number) => (
<ComponentPickerMenuItem
isSelected={selectedIndex === i}
onClick={() => {
if (option.disabled)
return
setHighlightedIndex(i)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
if (option.disabled)
return
setHighlightedIndex(i)
}}
key={option.key}
option={option}
/>
))}
</div>,
anchorElementRef.current,
)
: null}
triggerFn={checkForTriggerMatch}
/>
)
}
export default ComponentPicker
import type { FC } from 'react'
import {
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../utils'
import { CONTEXT_PLACEHOLDER_TEXT } from '../constants'
import {
$createContextBlockNode,
ContextBlockNode,
} from './context-block/node'
import type { ContextBlockProps } from './context-block/index'
import { CustomTextNode } from './custom-text/node'
const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
datasets,
onAddContext,
onInsert,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([ContextBlockNode]))
throw new Error('ContextBlockNodePlugin: ContextBlockNode not registered on editor')
}, [editor])
const createContextBlockNode = useCallback((): ContextBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext))
}, [datasets, onAddContext, onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
)
}, [])
return null
}
export default ContextBlockReplacementBlock
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectOrDelete, useTrigger } from '../../hooks'
import { UPDATE_DATASETS_EVENT_EMITTER } from '../../constants'
import type { Dataset } from './index'
import { DELETE_CONTEXT_BLOCK_COMMAND } from './index'
import { File05, Folder } from '@/app/components/base/icons/src/vender/solid/files'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type ContextBlockComponentProps = {
nodeKey: string
datasets?: Dataset[]
onAddContext: () => void
}
const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
nodeKey,
datasets = [],
onAddContext,
}) => {
const { t } = useTranslation()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND)
const [triggerRef, open, setOpen] = useTrigger()
const { eventEmitter } = useEventEmitterContextContext()
const [localDatasets, setLocalDatasets] = useState<Dataset[]>(datasets)
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_DATASETS_EVENT_EMITTER)
setLocalDatasets(v.payload)
})
return (
<div className={`
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent bg-[#F4F3FF] text-[#6938EF] rounded-[5px] hover:bg-[#EBE9FE]
${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'}
${isSelected && '!border-[#9B8AFB]'}
`} ref={ref}>
<File05 className='mr-1 w-[14px] h-[14px]' />
<div className='mr-1 text-xs font-medium'>{t('common.promptEditor.context.item.title')}</div>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 3,
alignmentAxis: -147,
}}
>
<PortalToFollowElemTrigger ref={triggerRef}>
<div className={`
flex items-center justify-center w-[18px] h-[18px] text-[11px] font-semibold rounded cursor-pointer
${open ? 'bg-[#6938EF] text-white' : 'bg-white/50 group-hover:bg-white group-hover:shadow-xs'}
`}>{localDatasets.length}</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 100 }}>
<div className='w-[360px] bg-white rounded-xl shadow-lg'>
<div className='p-4'>
<div className='mb-2 text-xs font-medium text-gray-500'>
{t('common.promptEditor.context.modal.title', { num: localDatasets.length })}
</div>
<div className='max-h-[270px] overflow-y-auto'>
{
localDatasets.map(dataset => (
<div key={dataset.id} className='flex items-center h-8'>
<div className='flex items-center justify-center shrink-0 mr-2 w-6 h-6 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#EAECF5]'>
<Folder className='w-4 h-4 text-[#444CE7]' />
</div>
<div className='text-sm text-gray-800 truncate' title=''>{dataset.name}</div>
</div>
))
}
</div>
<div className='flex items-center h-8 text-[#155EEF] cursor-pointer' onClick={onAddContext}>
<div className='shrink-0 flex justify-center items-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-gray-100'>
<Plus className='w-[14px] h-[14px]' />
</div>
<div className='text-[13px] font-medium' title=''>{t('common.promptEditor.context.modal.add')}</div>
</div>
</div>
<div className='px-4 py-3 text-xs text-gray-500 bg-gray-50 border-t-[0.5px] border-gray-50 rounded-b-xl'>
{t('common.promptEditor.context.modal.footer')}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default ContextBlockComponent
import type { FC } from 'react'
import { useEffect } from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$createContextBlockNode,
ContextBlockNode,
} from './node'
export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND')
export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND')
export type Dataset = {
id: string
name: string
type: string
}
export type ContextBlockProps = {
datasets: Dataset[]
onAddContext: () => void
onInsert?: () => void
onDelete?: () => void
}
const ContextBlock: FC<ContextBlockProps> = ({
datasets,
onAddContext,
onInsert,
onDelete,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([ContextBlockNode]))
throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_CONTEXT_BLOCK_COMMAND,
() => {
const contextBlockNode = $createContextBlockNode(datasets, onAddContext)
$insertNodes([contextBlockNode])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_CONTEXT_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, datasets, onAddContext, onInsert, onDelete])
return null
}
export default ContextBlock
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import ContextBlockComponent from './component'
import type { Dataset } from './index'
export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void }
export class ContextBlockNode extends DecoratorNode<JSX.Element> {
__datasets: Dataset[]
__onAddContext: () => void
static getType(): string {
return 'context-block'
}
static clone(node: ContextBlockNode): ContextBlockNode {
return new ContextBlockNode(node.__datasets, node.__onAddContext)
}
isInline(): boolean {
return true
}
constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey) {
super(key)
this.__datasets = datasets
this.__onAddContext = onAddContext
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): JSX.Element {
return (
<ContextBlockComponent
nodeKey={this.getKey()}
datasets={this.getDatasets()}
onAddContext={this.getOnAddContext()}
/>
)
}
getDatasets(): Dataset[] {
const self = this.getLatest()
return self.__datasets
}
getOnAddContext(): () => void {
const self = this.getLatest()
return self.__onAddContext
}
static importJSON(serializedNode: SerializedNode): ContextBlockNode {
const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext)
return node
}
exportJSON(): SerializedNode {
return {
type: 'context-block',
version: 1,
datasets: this.getDatasets(),
onAddContext: this.getOnAddContext(),
}
}
getTextContent(): string {
return '{{#context#}}'
}
}
export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void): ContextBlockNode {
return new ContextBlockNode(datasets, onAddContext)
}
export function $isContextBlockNode(
node: ContextBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof ContextBlockNode
}
import type { EditorConfig, NodeKey, SerializedTextNode } from 'lexical'
import { $createTextNode, TextNode } from 'lexical'
export class CustomTextNode extends TextNode {
static getType() {
return 'custom-text'
}
static clone(node: CustomTextNode) {
return new CustomTextNode(node.__text, node.__key)
}
constructor(text: string, key?: NodeKey) {
super(text, key)
}
createDOM(config: EditorConfig) {
const dom = super.createDOM(config)
dom.classList.add('align-middle')
return dom
}
static importJSON(serializedNode: SerializedTextNode): TextNode {
const node = $createTextNode(serializedNode.text)
node.setFormat(serializedNode.format)
node.setDetail(serializedNode.detail)
node.setMode(serializedNode.mode)
node.setStyle(serializedNode.style)
return node
}
exportJSON(): SerializedTextNode {
return {
detail: this.getDetail(),
format: this.getFormat(),
mode: this.getMode(),
style: this.getStyle(),
text: this.getTextContent(),
type: 'custom-text',
version: 1,
}
}
isSimpleText() {
return (
(this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0)
}
}
export function $createCustomTextNode(text: string): CustomTextNode {
return new CustomTextNode(text)
}
import type { FC } from 'react'
import {
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../utils'
import { HISTORY_PLACEHOLDER_TEXT } from '../constants'
import {
$createHistoryBlockNode,
HistoryBlockNode,
} from './history-block/node'
import type { HistoryBlockProps } from './history-block/index'
import { CustomTextNode } from './custom-text/node'
const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
roleName,
onEditRole,
onInsert,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([HistoryBlockNode]))
throw new Error('HistoryBlockNodePlugin: HistoryBlockNode not registered on editor')
}, [editor])
const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createHistoryBlockNode(roleName, onEditRole))
}, [roleName, onEditRole, onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
)
}, [])
return null
}
export default HistoryBlockReplacementBlock
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectOrDelete, useTrigger } from '../../hooks'
import { UPDATE_HISTORY_EVENT_EMITTER } from '../../constants'
import type { RoleName } from './index'
import { DELETE_HISTORY_BLOCK_COMMAND } from './index'
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type HistoryBlockComponentProps = {
nodeKey: string
roleName?: RoleName
onEditRole: () => void
}
const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
nodeKey,
roleName = { user: '', assistant: '' },
onEditRole,
}) => {
const { t } = useTranslation()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND)
const [triggerRef, open, setOpen] = useTrigger()
const { eventEmitter } = useEventEmitterContextContext()
const [localRoleName, setLocalRoleName] = useState<RoleName>(roleName)
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_HISTORY_EVENT_EMITTER)
setLocalRoleName(v.payload)
})
return (
<div className={`
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent text-[#DD2590] rounded-[5px] hover:bg-[#FCE7F6]
${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'}
${isSelected && '!border-[#F670C7]'}
`} ref={ref}>
<MessageClockCircle className='mr-1 w-[14px] h-[14px]' />
<div className='mr-1 text-xs font-medium'>{t('common.promptEditor.history.item.title')}</div>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='top-end'
offset={{
mainAxis: 4,
alignmentAxis: -148,
}}
>
<PortalToFollowElemTrigger ref={triggerRef}>
<div className={`
flex items-center justify-center w-[18px] h-[18px] rounded cursor-pointer
${open ? 'bg-[#DD2590] text-white' : 'bg-white/50 group-hover:bg-white group-hover:shadow-xs'}
`}>
<DotsHorizontal className='w-3 h-3' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 100 }}>
<div className='w-[360px] bg-white rounded-xl shadow-lg'>
<div className='p-4'>
<div className='mb-2 text-xs font-medium text-gray-500'>{t('common.promptEditor.history.modal.title')}</div>
<div className='flex items-center text-sm text-gray-700'>
<div className='mr-1 w-20 text-xs font-semibold'>{localRoleName?.user}</div>
{t('common.promptEditor.history.modal.user')}
</div>
<div className='flex items-center text-sm text-gray-700'>
<div className='mr-1 w-20 text-xs font-semibold'>{localRoleName?.assistant}</div>
{t('common.promptEditor.history.modal.assistant')}
</div>
</div>
<div
className='px-4 py-3 text-xs text-[#155EEF] border-t border-black/5 rounded-b-xl cursor-pointer'
onClick={onEditRole}
>
{t('common.promptEditor.history.modal.edit')}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default HistoryBlockComponent
import type { FC } from 'react'
import { useEffect } from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$createHistoryBlockNode,
HistoryBlockNode,
} from './node'
export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND')
export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND')
export type RoleName = {
user: string
assistant: string
}
export type HistoryBlockProps = {
roleName: RoleName
onEditRole: () => void
onInsert?: () => void
onDelete?: () => void
}
const HistoryBlock: FC<HistoryBlockProps> = ({
roleName,
onEditRole,
onInsert,
onDelete,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([HistoryBlockNode]))
throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_HISTORY_BLOCK_COMMAND,
() => {
const historyBlockNode = $createHistoryBlockNode(roleName, onEditRole)
$insertNodes([historyBlockNode])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_HISTORY_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, roleName, onEditRole, onInsert, onDelete])
return null
}
export default HistoryBlock
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import HistoryBlockComponent from './component'
import type { RoleName } from './index'
export type SerializedNode = SerializedLexicalNode & { roleName: RoleName; onEditRole: () => void }
export class HistoryBlockNode extends DecoratorNode<JSX.Element> {
__roleName: RoleName
__onEditRole: () => void
static getType(): string {
return 'history-block'
}
static clone(node: HistoryBlockNode): HistoryBlockNode {
return new HistoryBlockNode(node.__roleName, node.__onEditRole)
}
constructor(roleName: RoleName, onEditRole: () => void, key?: NodeKey) {
super(key)
this.__roleName = roleName
this.__onEditRole = onEditRole
}
isInline(): boolean {
return true
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): JSX.Element {
return (
<HistoryBlockComponent
nodeKey={this.getKey()}
roleName={this.getRoleName()}
onEditRole={this.getOnEditRole()}
/>
)
}
getRoleName(): RoleName {
const self = this.getLatest()
return self.__roleName
}
getOnEditRole(): () => void {
const self = this.getLatest()
return self.__onEditRole
}
static importJSON(serializedNode: SerializedNode): HistoryBlockNode {
const node = $createHistoryBlockNode(serializedNode.roleName, serializedNode.onEditRole)
return node
}
exportJSON(): SerializedNode {
return {
type: 'history-block',
version: 1,
roleName: this.getRoleName(),
onEditRole: this.getOnEditRole,
}
}
getTextContent(): string {
return '{{#histories#}}'
}
}
export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => void): HistoryBlockNode {
return new HistoryBlockNode(roleName, onEditRole)
}
export function $isHistoryBlockNode(
node: HistoryBlockNode | LexicalNode | null | undefined,
): node is HistoryBlockNode {
return node instanceof HistoryBlockNode
}
import type { FC } from 'react'
import { useEffect } from 'react'
import {
BLUR_COMMAND,
COMMAND_PRIORITY_EDITOR,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
type OnBlurBlockProps = {
onBlur?: () => void
}
const OnBlurBlock: FC<OnBlurBlockProps> = ({
onBlur,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
return mergeRegister(
editor.registerCommand(
BLUR_COMMAND,
() => {
if (onBlur)
onBlur()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onBlur])
return null
}
export default OnBlurBlock
import { useTranslation } from 'react-i18next'
const Placeholder = () => {
const { t } = useTranslation()
return (
<div className='absolute top-0 left-0 h-full w-full text-sm text-gray-300 select-none pointer-events-none leading-6'>
{t('common.promptEditor.placeholder')}
</div>
)
}
export default Placeholder
import type { FC } from 'react'
import {
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../utils'
import { QUERY_PLACEHOLDER_TEXT } from '../constants'
import {
$createQueryBlockNode,
QueryBlockNode,
} from './query-block/node'
import type { QueryBlockProps } from './query-block/index'
import { CustomTextNode } from './custom-text/node'
const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
onInsert,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([QueryBlockNode]))
throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor')
}, [editor])
const createQueryBlockNode = useCallback((): QueryBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createQueryBlockNode())
}, [onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
)
}, [])
return null
}
export default QueryBlockReplacementBlock
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_QUERY_BLOCK_COMMAND } from './index'
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
type QueryBlockComponentProps = {
nodeKey: string
}
const QueryBlockComponent: FC<QueryBlockComponentProps> = ({
nodeKey,
}) => {
const { t } = useTranslation()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
return (
<div
className={`
inline-flex items-center pl-1 pr-0.5 h-6 bg-[#FFF6ED] border border-transparent rounded-[5px] hover:bg-[#FFEAD5]
${isSelected && '!border-[#FD853A]'}
`}
ref={ref}
>
<UserEdit02 className='mr-1 w-[14px] h-[14px] text-[#FD853A]' />
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'{{'}</div>
<div className='text-xs font-medium text-[#EC4A0A]'>{t('common.promptEditor.query.item.title')}</div>
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'}}'}</div>
</div>
)
}
export default QueryBlockComponent
import type { FC } from 'react'
import { useEffect } from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$createQueryBlockNode,
QueryBlockNode,
} from './node'
export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND')
export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND')
export type QueryBlockProps = {
onInsert?: () => void
onDelete?: () => void
}
const QueryBlock: FC<QueryBlockProps> = ({
onInsert,
onDelete,
}) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([QueryBlockNode]))
throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_QUERY_BLOCK_COMMAND,
() => {
const contextBlockNode = $createQueryBlockNode()
$insertNodes([contextBlockNode])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_QUERY_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onInsert, onDelete])
return null
}
export default QueryBlock
import type { LexicalNode, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import QueryBlockComponent from './component'
export type SerializedNode = SerializedLexicalNode
export class QueryBlockNode extends DecoratorNode<JSX.Element> {
static getType(): string {
return 'query-block'
}
static clone(): QueryBlockNode {
return new QueryBlockNode()
}
isInline(): boolean {
return true
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): JSX.Element {
return <QueryBlockComponent nodeKey={this.getKey()} />
}
static importJSON(): QueryBlockNode {
const node = $createQueryBlockNode()
return node
}
exportJSON(): SerializedNode {
return {
type: 'query-block',
version: 1,
}
}
getTextContent(): string {
return '{{#query#}}'
}
}
export function $createQueryBlockNode(): QueryBlockNode {
return new QueryBlockNode()
}
export function $isQueryBlockNode(
node: QueryBlockNode | LexicalNode | null | undefined,
): node is QueryBlockNode {
return node instanceof QueryBlockNode
}
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { TreeView } from '@lexical/react/LexicalTreeView'
const TreeViewPlugin = () => {
const [editor] = useLexicalComposerContext()
return (
<TreeView
viewClassName="tree-view-output"
treeTypeButtonClassName="debug-treetype-button"
timeTravelPanelClassName="debug-timetravel-panel"
timeTravelButtonClassName="debug-timetravel-button"
timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
timeTravelPanelButtonClassName="debug-timetravel-panel-button"
editor={editor}
/>
)
}
export default TreeViewPlugin
import { useEffect } from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { CustomTextNode } from '../custom-text/node'
export const INSERT_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_BLOCK_COMMAND')
export const INSERT_VARIABLE_VALUE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_VALUE_BLOCK_COMMAND')
const VariableBlock = () => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
return mergeRegister(
editor.registerCommand(
INSERT_VARIABLE_BLOCK_COMMAND,
() => {
const textNode = new CustomTextNode('{')
$insertNodes([textNode])
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
(value: string) => {
const textNode = new CustomTextNode(value)
$insertNodes([textNode])
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor])
return null
}
export default VariableBlock
import type { FC } from 'react'
import { useCallback, useMemo, useState } from 'react'
import ReactDOM from 'react-dom'
import { useTranslation } from 'react-i18next'
import { $insertNodes, type TextNode } from 'lexical'
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
} from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useBasicTypeaheadTriggerMatch } from '../hooks'
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block'
import { $createCustomTextNode } from './custom-text/node'
import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
class VariablePickerOption extends MenuOption {
title: string
icon?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
constructor(
title: string,
options: {
icon?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.keyboardShortcut = options.keyboardShortcut
this.onSelect = options.onSelect.bind(this)
}
}
type VariablePickerMenuItemProps = {
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: VariablePickerOption
queryString: string | null
}
const VariablePickerMenuItem: FC<VariablePickerMenuItemProps> = ({
isSelected,
onClick,
onMouseEnter,
option,
queryString,
}) => {
const title = option.title
let before = title
let middle = ''
let after = ''
if (queryString) {
const regex = new RegExp(queryString, 'i')
const match = regex.exec(option.title)
if (match) {
before = title.substring(0, match.index)
middle = match[0]
after = title.substring(match.index + match[0].length)
}
}
return (
<div
key={option.key}
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${isSelected && 'bg-primary-50'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<div className='mr-2'>
{option.icon}
</div>
<div className='text-[13px] text-gray-900'>
{before}
<span className='text-[#2970FF]'>{middle}</span>
{after}
</div>
</div>
)
}
export type Option = {
value: string
name: string
}
type VariablePickerProps = {
items?: Option[]
}
const VariablePicker: FC<VariablePickerProps> = ({
items = [],
}) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', {
minLength: 0,
maxLength: 6,
})
const [queryString, setQueryString] = useState<string | null>(null)
const options = useMemo(() => {
const baseOptions = items.map((item) => {
return new VariablePickerOption(item.value, {
icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
},
})
})
if (!queryString)
return baseOptions
const regex = new RegExp(queryString, 'i')
return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
}, [editor, queryString, items])
const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), {
icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
onSelect: () => {
editor.update(() => {
const prefixNode = $createCustomTextNode('{{')
const suffixNode = $createCustomTextNode('}}')
$insertNodes([prefixNode, suffixNode])
prefixNode.select()
})
},
})
const onSelectOption = useCallback(
(
selectedOption: VariablePickerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
matchingString: string,
) => {
editor.update(() => {
if (nodeToRemove)
nodeToRemove.remove()
selectedOption.onSelect(matchingString)
closeMenu()
})
},
[editor],
)
const mergedOptions = [...options, newOption]
return (
<LexicalTypeaheadMenuPlugin
options={mergedOptions}
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
) =>
(anchorElementRef.current && mergedOptions.length)
? ReactDOM.createPortal(
<div className='mt-[25px] w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
{
!!options.length && (
<>
<div className='p-1'>
{options.map((option, i: number) => (
<VariablePickerMenuItem
isSelected={selectedIndex === i}
onClick={() => {
setHighlightedIndex(i)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
setHighlightedIndex(i)
}}
key={option.key}
option={option}
queryString={queryString}
/>
))}
</div>
<div className='h-[1px] bg-gray-100' />
</>
)
}
<div className='p-1'>
<div
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${selectedIndex === options.length && 'bg-primary-50'}
`}
ref={newOption.setRefElement}
tabIndex={-1}
onClick={() => {
setHighlightedIndex(options.length)
selectOptionAndCleanUp(newOption)
}}
onMouseEnter={() => {
setHighlightedIndex(options.length)
}}
key={newOption.key}
>
{newOption.icon}
<div className='text-[13px] text-gray-900'>{newOption.title}</div>
</div>
</div>
</div>,
anchorElementRef.current,
)
: null}
triggerFn={checkForTriggerMatch}
/>
)
}
export default VariablePicker
import {
useCallback,
useEffect,
} from 'react'
import type { TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useLexicalTextEntity } from '../../hooks'
import {
$createVariableValueBlockNode,
VariableValueBlockNode,
} from './node'
import { getHashtagRegexString } from './utils'
const REGEX = new RegExp(getHashtagRegexString(), 'i')
const VariableValueBlock = () => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([VariableValueBlockNode]))
throw new Error('VariableValueBlockPlugin: VariableValueNode not registered on editor')
}, [editor])
const createVariableValueBlockNode = useCallback((textNode: TextNode): VariableValueBlockNode => {
return $createVariableValueBlockNode(textNode.getTextContent())
}, [])
const getVariableValueMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const hashtagLength = matchArr[3].length + 4
const startOffset = matchArr.index
const endOffset = startOffset + hashtagLength
return {
end: endOffset,
start: startOffset,
}
}, [])
useLexicalTextEntity<VariableValueBlockNode>(
getVariableValueMatch,
VariableValueBlockNode,
createVariableValueBlockNode,
)
return null
}
export default VariableValueBlock
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical'
import {
$applyNodeReplacement,
TextNode,
} from 'lexical'
export class VariableValueBlockNode extends TextNode {
static getType(): string {
return 'variable-value-block'
}
static clone(node: VariableValueBlockNode): VariableValueBlockNode {
return new VariableValueBlockNode(node.__text, node.__key)
}
constructor(text: string, key?: NodeKey) {
super(text, key)
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config)
element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-[#155EEF]', 'rounded-[5px]', 'align-middle')
return element
}
static importJSON(serializedNode: SerializedTextNode): TextNode {
const node = $createVariableValueBlockNode(serializedNode.text)
node.setFormat(serializedNode.format)
node.setDetail(serializedNode.detail)
node.setMode(serializedNode.mode)
node.setStyle(serializedNode.style)
return node
}
exportJSON(): SerializedTextNode {
return {
detail: this.getDetail(),
format: this.getFormat(),
mode: this.getMode(),
style: this.getStyle(),
text: this.getTextContent(),
type: 'variable-value-block',
version: 1,
}
}
canInsertTextBefore(): boolean {
return false
}
}
export function $createVariableValueBlockNode(text = ''): VariableValueBlockNode {
return $applyNodeReplacement(new VariableValueBlockNode(text))
}
export function $isVariableValueNodeBlock(
node: LexicalNode | null | undefined,
): node is VariableValueBlockNode {
return node instanceof VariableValueBlockNode
}
function getHashtagRegexStringChars(): Readonly<{
alpha: string
alphanumeric: string
leftChars: string
rightChars: string
}> {
// Latin accented characters
// Excludes 0xd7 from the range
// (the multiplication sign, confusable with "x").
// Also excludes 0xf7, the division sign
const latinAccents
= '\xC0-\xD6'
+ '\xD8-\xF6'
+ '\xF8-\xFF'
+ '\u0100-\u024F'
+ '\u0253-\u0254'
+ '\u0256-\u0257'
+ '\u0259'
+ '\u025B'
+ '\u0263'
+ '\u0268'
+ '\u026F'
+ '\u0272'
+ '\u0289'
+ '\u028B'
+ '\u02BB'
+ '\u0300-\u036F'
+ '\u1E00-\u1EFF'
// Cyrillic (Russian, Ukrainian, etc.)
const nonLatinChars
= '\u0400-\u04FF' // Cyrillic
+ '\u0500-\u0527' // Cyrillic Supplement
+ '\u2DE0-\u2DFF' // Cyrillic Extended A
+ '\uA640-\uA69F' // Cyrillic Extended B
+ '\u0591-\u05BF' // Hebrew
+ '\u05C1-\u05C2'
+ '\u05C4-\u05C5'
+ '\u05C7'
+ '\u05D0-\u05EA'
+ '\u05F0-\u05F4'
+ '\uFB12-\uFB28' // Hebrew Presentation Forms
+ '\uFB2A-\uFB36'
+ '\uFB38-\uFB3C'
+ '\uFB3E'
+ '\uFB40-\uFB41'
+ '\uFB43-\uFB44'
+ '\uFB46-\uFB4F'
+ '\u0610-\u061A' // Arabic
+ '\u0620-\u065F'
+ '\u066E-\u06D3'
+ '\u06D5-\u06DC'
+ '\u06DE-\u06E8'
+ '\u06EA-\u06EF'
+ '\u06FA-\u06FC'
+ '\u06FF'
+ '\u0750-\u077F' // Arabic Supplement
+ '\u08A0' // Arabic Extended A
+ '\u08A2-\u08AC'
+ '\u08E4-\u08FE'
+ '\uFB50-\uFBB1' // Arabic Pres. Forms A
+ '\uFBD3-\uFD3D'
+ '\uFD50-\uFD8F'
+ '\uFD92-\uFDC7'
+ '\uFDF0-\uFDFB'
+ '\uFE70-\uFE74' // Arabic Pres. Forms B
+ '\uFE76-\uFEFC'
+ '\u200C-\u200C' // Zero-Width Non-Joiner
+ '\u0E01-\u0E3A' // Thai
+ '\u0E40-\u0E4E' // Hangul (Korean)
+ '\u1100-\u11FF' // Hangul Jamo
+ '\u3130-\u3185' // Hangul Compatibility Jamo
+ '\uA960-\uA97F' // Hangul Jamo Extended-A
+ '\uAC00-\uD7AF' // Hangul Syllables
+ '\uD7B0-\uD7FF' // Hangul Jamo Extended-B
+ '\uFFA1-\uFFDC' // Half-width Hangul
const charCode = String.fromCharCode
const cjkChars
= '\u30A1-\u30FA\u30FC-\u30FE' // Katakana (full-width)
+ '\uFF66-\uFF9F' // Katakana (half-width)
+ '\uFF10-\uFF19\uFF21-\uFF3A'
+ '\uFF41-\uFF5A' // Latin (full-width)
+ '\u3041-\u3096\u3099-\u309E' // Hiragana
+ '\u3400-\u4DBF' // Kanji (CJK Extension A)
+ `\u4E00-\u9FFF${// Kanji (Unified)
// Disabled as it breaks the Regex.
// charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B)
charCode(0x2A700)
}-${
charCode(0x2B73F) // Kanji (CJK Extension C)
}${charCode(0x2B740)
}-${
charCode(0x2B81F) // Kanji (CJK Extension D)
}${charCode(0x2F800)
}-${
charCode(0x2FA1F)
}\u3003\u3005\u303B` // Kanji (CJK supplement)
const otherChars = latinAccents + nonLatinChars + cjkChars
// equivalent of \p{L}
const unicodeLetters
= '\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6'
+ '\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386'
+ '\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481'
+ '\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587'
+ '\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F'
+ '\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710'
+ '\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950'
+ '\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0'
+ '\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1'
+ '\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33'
+ '\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D'
+ '\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD'
+ '\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30'
+ '\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83'
+ '\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F'
+ '\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10'
+ '\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C'
+ '\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE'
+ '\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39'
+ '\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6'
+ '\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88'
+ '\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7'
+ '\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6'
+ '\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021'
+ '\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC'
+ '\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D'
+ '\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0'
+ '\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310'
+ '\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C'
+ '\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711'
+ '\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7'
+ '\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974'
+ '\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B'
+ '\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D'
+ '\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC'
+ '\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC'
+ '\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107'
+ '\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D'
+ '\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E'
+ '\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96'
+ '\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6'
+ '\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035'
+ '\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF'
+ '\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5'
+ '\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A'
+ '\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9'
+ '\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C'
+ '\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F'
+ '\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A'
+ '\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7'
+ '\uFFDA-\uFFDC'
// equivalent of \p{Mn}\p{Mc}
const unicodeAccents
= '\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF'
+ '\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670'
+ '\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A'
+ '\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963'
+ '\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7'
+ '\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D'
+ '\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD'
+ '\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D'
+ '\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7'
+ '\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56'
+ '\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6'
+ '\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83'
+ '\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A'
+ '\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19'
+ '\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97'
+ '\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F'
+ '\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD'
+ '\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9'
+ '\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F'
+ '\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F'
+ '\uFE20-\uFE23'
// equivalent of \p{Dn}
const unicodeDigits
= '\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF'
+ '\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F'
+ '\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29'
+ '\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9'
+ '\uFF10-\uFF19'
// An alpha char is a unicode chars including unicode marks or
// letter or char in otherChars range
const alpha = unicodeLetters
// A numeric character is any with the number digit property, or
// underscore. These characters can be included in hashtags, but a hashtag
// cannot have only these characters.
const numeric = `${unicodeDigits}_`
// Alphanumeric char is any alpha char or a unicode char with decimal
// number property \p{Nd}
const alphanumeric = alpha + numeric
const leftChars = '{'
const rightChars = '}'
return {
alpha,
alphanumeric,
leftChars,
rightChars,
}
}
export function getHashtagRegexString(): string {
const { alpha, alphanumeric, leftChars, rightChars } = getHashtagRegexStringChars()
const hashtagAlpha = `[${alpha}]`
const hashtagAlphanumeric = `[${alphanumeric}]`
const hashLeftCharList = `[${leftChars}]`
const hashRightCharList = `[${rightChars}]`
// A hashtag contains characters, numbers and underscores,
// but not all numbers.
const hashtag
= `(${
hashLeftCharList
})`
+ `(${
hashLeftCharList
})([a-zA-Z_][a-zA-Z0-9_]{0,29}`
+ `)(${
hashRightCharList
})(${
hashRightCharList
})`
return hashtag
}
import { $isAtNodeEnd } from '@lexical/selection'
import type {
ElementNode,
Klass,
LexicalEditor,
LexicalNode,
RangeSelection,
TextNode,
} from 'lexical'
import {
$createTextNode,
$isTextNode,
} from 'lexical'
import type { EntityMatch } from '@lexical/text'
import { CustomTextNode } from './plugins/custom-text/node'
export function getSelectedNode(
selection: RangeSelection,
): TextNode | ElementNode {
const anchor = selection.anchor
const focus = selection.focus
const anchorNode = selection.anchor.getNode()
const focusNode = selection.focus.getNode()
if (anchorNode === focusNode)
return anchorNode
const isBackward = selection.isBackward()
if (isBackward)
return $isAtNodeEnd(focus) ? anchorNode : focusNode
else
return $isAtNodeEnd(anchor) ? anchorNode : focusNode
}
export function registerLexicalTextEntity<T extends TextNode>(
editor: LexicalEditor,
getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>,
createNode: (textNode: TextNode) => T,
) {
const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
return node instanceof targetNode
}
const replaceWithSimpleText = (node: TextNode): void => {
const textNode = $createTextNode(node.getTextContent())
textNode.setFormat(node.getFormat())
node.replace(textNode)
}
const getMode = (node: TextNode): number => {
return node.getLatest().__mode
}
const textNodeTransform = (node: TextNode) => {
if (!node.isSimpleText())
return
const prevSibling = node.getPreviousSibling()
let text = node.getTextContent()
let currentNode = node
let match
if ($isTextNode(prevSibling)) {
const previousText = prevSibling.getTextContent()
const combinedText = previousText + text
const prevMatch = getMatch(combinedText)
if (isTargetNode(prevSibling)) {
if (prevMatch === null || getMode(prevSibling) !== 0) {
replaceWithSimpleText(prevSibling)
return
}
else {
const diff = prevMatch.end - previousText.length
if (diff > 0) {
const concatText = text.slice(0, diff)
const newTextContent = previousText + concatText
prevSibling.select()
prevSibling.setTextContent(newTextContent)
if (diff === text.length) {
node.remove()
}
else {
const remainingText = text.slice(diff)
node.setTextContent(remainingText)
}
return
}
}
}
else if (prevMatch === null || prevMatch.start < previousText.length) {
return
}
}
while (true) {
match = getMatch(text)
let nextText = match === null ? '' : text.slice(match.end)
text = nextText
if (nextText === '') {
const nextSibling = currentNode.getNextSibling()
if ($isTextNode(nextSibling)) {
nextText = currentNode.getTextContent() + nextSibling.getTextContent()
const nextMatch = getMatch(nextText)
if (nextMatch === null) {
if (isTargetNode(nextSibling))
replaceWithSimpleText(nextSibling)
else
nextSibling.markDirty()
return
}
else if (nextMatch.start !== 0) {
return
}
}
}
else {
const nextMatch = getMatch(nextText)
if (nextMatch !== null && nextMatch.start === 0)
return
}
if (match === null)
return
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
continue
let nodeToReplace
if (match.start === 0)
[nodeToReplace, currentNode] = currentNode.splitText(match.end)
else
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
const replacementNode = createNode(nodeToReplace)
replacementNode.setFormat(nodeToReplace.getFormat())
nodeToReplace.replace(replacementNode)
if (currentNode == null)
return
}
}
const reverseNodeTransform = (node: T) => {
const text = node.getTextContent()
const match = getMatch(text)
if (match === null || match.start !== 0) {
replaceWithSimpleText(node)
return
}
if (text.length > match.end) {
// This will split out the rest of the text as simple text
node.splitText(match.end)
return
}
const prevSibling = node.getPreviousSibling()
if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {
replaceWithSimpleText(prevSibling)
replaceWithSimpleText(node)
}
const nextSibling = node.getNextSibling()
if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block
if (isTargetNode(node))
replaceWithSimpleText(node)
}
}
const removePlainTextTransform = editor.registerNodeTransform(CustomTextNode, textNodeTransform)
const removeReverseNodeTransform = editor.registerNodeTransform(targetNode, reverseNodeTransform)
return [removePlainTextTransform, removeReverseNodeTransform]
}
export const decoratorTransform = (
node: CustomTextNode,
getMatch: (text: string) => null | EntityMatch,
createNode: () => LexicalNode,
) => {
if (!node.isSimpleText())
return
const prevSibling = node.getPreviousSibling()
let text = node.getTextContent()
let currentNode = node
let match
while (true) {
match = getMatch(text)
let nextText = match === null ? '' : text.slice(match.end)
text = nextText
if (nextText === '') {
const nextSibling = currentNode.getNextSibling()
if ($isTextNode(nextSibling)) {
nextText = currentNode.getTextContent() + nextSibling.getTextContent()
const nextMatch = getMatch(nextText)
if (nextMatch === null) {
nextSibling.markDirty()
return
}
else if (nextMatch.start !== 0) {
return
}
}
}
else {
const nextMatch = getMatch(nextText)
if (nextMatch !== null && nextMatch.start === 0)
return
}
if (match === null)
return
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
continue
let nodeToReplace
if (match.start === 0)
[nodeToReplace, currentNode] = currentNode.splitText(match.end)
else
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
const replacementNode = createNode()
nodeToReplace.replace(replacementNode)
if (currentNode == null)
return
}
}
export function textToEditorState(text: string) {
const paragraph = text.split('\n')
return JSON.stringify({
root: {
children: paragraph.map((p) => {
return {
children: [{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: p,
type: 'custom-text',
version: 1,
}],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
}
}),
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
})
}
import type { FC } from 'react'
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
type CardProps = {
log: { role: string; text: string }[]
}
const Card: FC<CardProps> = ({
log,
}) => {
return (
<>
{
log.length === 1 && (
<div className='px-4 py-2'>
<div className='whitespace-pre-line text-gray-700'>
{log[0].text}
</div>
</div>
)
}
{
log.length > 1 && (
<div>
{
log.map((item, index) => (
<div key={index} className='group/card mb-2 px-4 pt-2 pb-4 rounded-xl hover:bg-gray-50 last-of-type:mb-0'>
<div className='flex justify-between items-center h-8'>
<div className='font-semibold text-[#2D31A6]'>{item.role.toUpperCase()}</div>
<CopyFeedbackNew className='hidden w-6 h-6 group-hover/card:block' content={item.text} />
</div>
<div className='whitespace-pre-line text-gray-700'>{item.text}</div>
</div>
))
}
</div>
)
}
</>
)
}
export default Card
import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useClickAway } from 'ahooks'
import Card from './card'
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
type PromptLogModalProps = {
log: { role: string; text: string }[]
width: number
onCancel: () => void
}
const PromptLogModal: FC<PromptLogModalProps> = ({
log,
width,
onCancel,
}) => {
const ref = useRef(null)
const [mounted, setMounted] = useState(false)
useClickAway(() => {
if (mounted)
onCancel()
}, ref)
useEffect(() => {
setMounted(true)
}, [])
return (
<div
className='fixed top-16 left-2 bottom-2 flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10'
style={{ width }}
ref={ref}
>
<div className='shrink-0 flex justify-between items-center pl-6 pr-5 h-14 border-b border-b-gray-100'>
<div className='text-base font-semibold text-gray-900'>PROMPT LOG</div>
<div className='flex items-center'>
{
log.length === 1 && (
<>
<CopyFeedbackNew className='w-6 h-6' content={log[0].text} />
<div className='mx-2.5 w-[1px] h-[14px] bg-gray-200' />
</>
)
}
<div
onClick={onCancel}
className='flex justify-center items-center w-6 h-6 cursor-pointer'
>
<XClose className='w-4 h-4 text-gray-500' />
</div>
</div>
</div>
<div className='grow p-2 overflow-y-auto'>
<Card log={log} />
</div>
</div>
)
}
export default PromptLogModal
...@@ -5,11 +5,13 @@ type ISliderProps = { ...@@ -5,11 +5,13 @@ type ISliderProps = {
max?: number max?: number
min?: number min?: number
step?: number step?: number
disabled?: boolean
onChange: (value: number) => void onChange: (value: number) => void
} }
const Slider: React.FC<ISliderProps> = ({ max, min, step, value, onChange }) => { const Slider: React.FC<ISliderProps> = ({ max, min, step, value, disabled, onChange }) => {
return <ReactSlider return <ReactSlider
disabled={disabled}
value={isNaN(value) ? 0 : value} value={isNaN(value) ? 0 : value}
min={min || 0} min={min || 0}
max={max || 100} max={max || 100}
......
.slider { .slider {
position: relative; position: relative;
} }
.slider.disabled {
opacity: 0.6;
}
.slider-thumb { .slider-thumb {
width: 18px; width: 18px;
...@@ -19,7 +22,7 @@ ...@@ -19,7 +22,7 @@
} }
.slider-track { .slider-track {
background-color: #9CA3AF; background-color: #528BFF;
height: 2px; height: 2px;
} }
......
import { useState } from 'react' import { useState } from 'react'
import type { ChangeEvent, FC, KeyboardEvent } from 'react' import type { ChangeEvent, FC, KeyboardEvent } from 'react'
import {} from 'use-context-selector' import { } from 'use-context-selector'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AutosizeInput from 'react-18-input-autosize' import AutosizeInput from 'react-18-input-autosize'
import cn from 'classnames'
import { X } from '@/app/components/base/icons/src/vender/line/general' import { X } from '@/app/components/base/icons/src/vender/line/general'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
...@@ -11,6 +12,7 @@ type TagInputProps = { ...@@ -11,6 +12,7 @@ type TagInputProps = {
onChange: (items: string[]) => void onChange: (items: string[]) => void
disableRemove?: boolean disableRemove?: boolean
disableAdd?: boolean disableAdd?: boolean
customizedConfirmKey?: 'Enter' | 'Tab'
} }
const TagInput: FC<TagInputProps> = ({ const TagInput: FC<TagInputProps> = ({
...@@ -18,11 +20,15 @@ const TagInput: FC<TagInputProps> = ({ ...@@ -18,11 +20,15 @@ const TagInput: FC<TagInputProps> = ({
onChange, onChange,
disableAdd, disableAdd,
disableRemove, disableRemove,
customizedConfirmKey = 'Enter',
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const [value, setValue] = useState('') const [value, setValue] = useState('')
const [focused, setFocused] = useState(false) const [focused, setFocused] = useState(false)
const isSpecialMode = customizedConfirmKey === 'Tab'
const handleRemove = (index: number) => { const handleRemove = (index: number) => {
const copyItems = [...items] const copyItems = [...items]
copyItems.splice(index, 1) copyItems.splice(index, 1)
...@@ -31,7 +37,13 @@ const TagInput: FC<TagInputProps> = ({ ...@@ -31,7 +37,13 @@ const TagInput: FC<TagInputProps> = ({
} }
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') { if (isSpecialMode && e.key === 'Enter')
setValue(`${value}↵`)
if (e.key === customizedConfirmKey) {
if (isSpecialMode)
e.preventDefault()
const valueTrimed = value.trim() const valueTrimed = value.trim()
if (!valueTrimed || (items.find(item => item === valueTrimed))) if (!valueTrimed || (items.find(item => item === valueTrimed)))
return return
...@@ -52,12 +64,12 @@ const TagInput: FC<TagInputProps> = ({ ...@@ -52,12 +64,12 @@ const TagInput: FC<TagInputProps> = ({
} }
return ( return (
<div className='flex flex-wrap'> <div className={cn('flex flex-wrap', isSpecialMode ? 'bg-gray-100 min-w-[200px] rounded-lg pb-1 pl-1' : '')}>
{ {
items.map((item, index) => ( items.map((item, index) => (
<div <div
key={item} key={item}
className='flex items-center mr-1 mt-1 px-2 py-1 text-sm text-gray-700 rounded-lg border border-gray-200'> className={cn('flex items-center mr-1 mt-1 px-2 py-1 text-sm text-gray-700 border border-gray-200', isSpecialMode ? 'bg-white rounded-md' : 'rounded-lg')}>
{item} {item}
{ {
!disableRemove && ( !disableRemove && (
...@@ -73,7 +85,7 @@ const TagInput: FC<TagInputProps> = ({ ...@@ -73,7 +85,7 @@ const TagInput: FC<TagInputProps> = ({
{ {
!disableAdd && ( !disableAdd && (
<AutosizeInput <AutosizeInput
inputClassName='outline-none appearance-none placeholder:text-gray-300 caret-primary-600 hover:placeholder:text-gray-400' inputClassName={cn('outline-none appearance-none placeholder:text-gray-300 caret-primary-600 hover:placeholder:text-gray-400', isSpecialMode ? 'bg-transparent' : '')}
className={` className={`
mt-1 py-1 rounded-lg border border-transparent text-sm max-w-[300px] overflow-hidden mt-1 py-1 rounded-lg border border-transparent text-sm max-w-[300px] overflow-hidden
${focused && 'px-2 border !border-dashed !border-gray-200'} ${focused && 'px-2 border !border-dashed !border-gray-200'}
...@@ -81,9 +93,11 @@ const TagInput: FC<TagInputProps> = ({ ...@@ -81,9 +93,11 @@ const TagInput: FC<TagInputProps> = ({
onFocus={() => setFocused(true)} onFocus={() => setFocused(true)}
onBlur={handleBlur} onBlur={handleBlur}
value={value} value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={t('datasetDocuments.segment.addKeyWord')} placeholder={t(isSpecialMode ? 'common.model.params.stop_sequencesPlaceholder' : 'datasetDocuments.segment.addKeyWord')}
/> />
) )
} }
......
...@@ -72,7 +72,6 @@ const DatasetConfig: FC<Props> = ({ ...@@ -72,7 +72,6 @@ const DatasetConfig: FC<Props> = ({
config={item} config={item}
onRemove={onRemove} onRemove={onRemove}
readonly={readonly} readonly={readonly}
// TODO: readonly remove btn
/> />
))} ))}
</div> </div>
......
...@@ -116,7 +116,7 @@ export default function AccountSetting({ ...@@ -116,7 +116,7 @@ export default function AccountSetting({
isShow isShow
onClose={() => { }} onClose={() => { }}
className={s.modal} className={s.modal}
wrapperClassName='pt-[60px]' wrapperClassName='!z-20 pt-[60px]'
> >
<div className='flex'> <div className='flex'>
<div className='w-[200px] p-4 border border-gray-100'> <div className='w-[200px] p-4 border border-gray-100'>
......
...@@ -121,6 +121,13 @@ const config: ProviderConfig = { ...@@ -121,6 +121,13 @@ const config: ProviderConfig = {
'zh-Hans': '文本生成', 'zh-Hans': '文本生成',
}, },
}, },
// {
// key: 'chat',
// label: {
// 'en': 'Chat',
// 'zh-Hans': '聊天',
// },
// },
{ {
key: 'embeddings', key: 'embeddings',
label: { label: {
...@@ -217,7 +224,17 @@ const config: ProviderConfig = { ...@@ -217,7 +224,17 @@ const config: ProviderConfig = {
'en': 'Task', 'en': 'Task',
'zh-Hans': 'Task', 'zh-Hans': 'Task',
}, },
options: [ options: (value?: FormValue) => {
if (value?.model_type === 'chat') {
return [{
key: 'question-answer',
label: {
'en': '问答',
'zh-Hans': 'Question Answer',
},
}]
}
return [
{ {
key: 'text2text-generation', key: 'text2text-generation',
label: { label: {
...@@ -232,7 +249,8 @@ const config: ProviderConfig = { ...@@ -232,7 +249,8 @@ const config: ProviderConfig = {
'zh-Hans': 'Text Generation', 'zh-Hans': 'Text Generation',
}, },
}, },
], ]
},
}, },
{ {
hidden: (value?: FormValue) => !(value?.huggingfacehub_api_type === 'inference_endpoints' && value?.model_type === 'embeddings'), hidden: (value?: FormValue) => !(value?.huggingfacehub_api_type === 'inference_endpoints' && value?.model_type === 'embeddings'),
......
...@@ -61,6 +61,13 @@ const config: ProviderConfig = { ...@@ -61,6 +61,13 @@ const config: ProviderConfig = {
'zh-Hans': '文本生成', 'zh-Hans': '文本生成',
}, },
}, },
// {
// key: 'chat',
// label: {
// 'en': 'Chat',
// 'zh-Hans': '聊天',
// },
// },
{ {
key: 'embeddings', key: 'embeddings',
label: { label: {
......
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import type { ModelModeType } from '@/types/app'
export type FormValue = Record<string, string> export type FormValue = Record<string, string>
...@@ -77,6 +78,7 @@ export type BackendModel = { ...@@ -77,6 +78,7 @@ export type BackendModel = {
model_name: string model_name: string
model_display_name: string // not always exist model_display_name: string // not always exist
model_type: ModelType model_type: ModelType
model_mode: ModelModeType
model_provider: { model_provider: {
provider_name: ProviderEnum provider_name: ProviderEnum
provider_type: PreferredProviderTypeEnum provider_type: PreferredProviderTypeEnum
......
...@@ -108,6 +108,15 @@ const Form: FC<FormProps> = ({ ...@@ -108,6 +108,15 @@ const Form: FC<FormProps> = ({
) )
extraValue.task_type = 'text-generation' extraValue.task_type = 'text-generation'
if (
(
(k === 'model_type' && v === 'chat' && value.huggingfacehub_api_type === 'inference_endpoints')
|| (k === 'huggingfacehub_api_type' && v === 'inference_endpoints' && value.model_type === 'chat')
)
&& modelModal?.key === ProviderEnum.huggingface_hub
)
extraValue.task_type = 'question-answer'
handleMultiFormChange({ ...value, [k]: v, ...extraValue }, k) handleMultiFormChange({ ...value, [k]: v, ...extraValue }, k)
} }
} }
......
...@@ -4,6 +4,7 @@ import { Popover, Transition } from '@headlessui/react' ...@@ -4,6 +4,7 @@ import { Popover, Transition } from '@headlessui/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import _ from 'lodash-es' import _ from 'lodash-es'
import cn from 'classnames' import cn from 'classnames'
import s from './style.module.css'
import type { BackendModel, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations' import type { BackendModel, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations' import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows' import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
...@@ -15,6 +16,10 @@ import ModelIcon from '@/app/components/app/configuration/config-model/model-ico ...@@ -15,6 +16,10 @@ import ModelIcon from '@/app/components/app/configuration/config-model/model-ico
import ModelName, { supportI18nModelName } from '@/app/components/app/configuration/config-model/model-name' import ModelName, { supportI18nModelName } from '@/app/components/app/configuration/config-model/model-name'
import ProviderName from '@/app/components/app/configuration/config-model/provider-name' import ProviderName from '@/app/components/app/configuration/config-model/provider-name'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
import type { ModelModeType } from '@/types/app'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import AccountSetting from '@/app/components/header/account-setting'
type Props = { type Props = {
value: { value: {
...@@ -22,6 +27,8 @@ type Props = { ...@@ -22,6 +27,8 @@ type Props = {
modelName: string modelName: string
} | undefined } | undefined
modelType: ModelType modelType: ModelType
isShowModelModeType?: boolean
isShowAddModel?: boolean
supportAgentThought?: boolean supportAgentThought?: boolean
onChange: (value: BackendModel) => void onChange: (value: BackendModel) => void
popClassName?: string popClassName?: string
...@@ -34,6 +41,7 @@ type ModelOption = { ...@@ -34,6 +41,7 @@ type ModelOption = {
value: string value: string
providerName: ProviderEnum providerName: ProviderEnum
modelDisplayName: string modelDisplayName: string
model_mode: ModelModeType
} | { } | {
type: 'provider' type: 'provider'
value: ProviderEnum value: ProviderEnum
...@@ -42,6 +50,8 @@ type ModelOption = { ...@@ -42,6 +50,8 @@ type ModelOption = {
const ModelSelector: FC<Props> = ({ const ModelSelector: FC<Props> = ({
value, value,
modelType, modelType,
isShowModelModeType,
isShowAddModel,
supportAgentThought, supportAgentThought,
onChange, onChange,
popClassName, popClassName,
...@@ -89,18 +99,21 @@ const ModelSelector: FC<Props> = ({ ...@@ -89,18 +99,21 @@ const ModelSelector: FC<Props> = ({
value: providerName, value: providerName,
}) })
const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName) const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
models.forEach(({ model_name, model_display_name }) => { models.forEach(({ model_name, model_display_name, model_mode }) => {
res.push({ res.push({
type: 'model', type: 'model',
providerName, providerName,
value: model_name, value: model_name,
modelDisplayName: model_display_name, modelDisplayName: model_display_name,
model_mode,
}) })
}) })
}) })
return res return res
})() })()
const [showSettingModal, setShowSettingModal] = useState(false)
return ( return (
<div className=''> <div className=''>
<Popover className='relative'> <Popover className='relative'>
...@@ -117,7 +130,12 @@ const ModelSelector: FC<Props> = ({ ...@@ -117,7 +130,12 @@ const ModelSelector: FC<Props> = ({
modelId={value.modelName} modelId={value.modelName}
providerName={value.providerName} providerName={value.providerName}
/> />
<div className='mr-1.5 grow text-left text-sm text-gray-900 truncate'><ModelName modelId={value.modelName} modelDisplayName={currModel?.model_display_name} /></div> <div className='mr-1.5 grow flex items-center text-left text-sm text-gray-900 truncate'>
<ModelName modelId={value.modelName} modelDisplayName={currModel?.model_display_name} />
{isShowModelModeType && (
<ModelModeTypeLabel className='ml-2' type={currModel?.model_mode as ModelModeType} />
)}
</div>
</> </>
) )
: ( : (
...@@ -148,7 +166,7 @@ const ModelSelector: FC<Props> = ({ ...@@ -148,7 +166,7 @@ const ModelSelector: FC<Props> = ({
leaveFrom='opacity-100' leaveFrom='opacity-100'
leaveTo='opacity-0' leaveTo='opacity-0'
> >
<Popover.Panel className={cn(popClassName, 'absolute top-10 p-1 min-w-[232px] max-w-[260px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-10')}> <Popover.Panel className={cn(popClassName, isShowModelModeType ? 'max-w-[312px]' : 'max-w-[260px]', 'absolute top-10 p-1 min-w-[232px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-10')}>
<div className='px-2 pt-2 pb-1'> <div className='px-2 pt-2 pb-1'>
<div className='flex items-center px-2 h-8 bg-gray-100 rounded-lg'> <div className='flex items-center px-2 h-8 bg-gray-100 rounded-lg'>
<div className='mr-1.5 p-[1px]'><SearchLg className='w-[14px] h-[14px] text-gray-400' /></div> <div className='mr-1.5 p-[1px]'><SearchLg className='w-[14px] h-[14px] text-gray-400' /></div>
...@@ -189,7 +207,7 @@ const ModelSelector: FC<Props> = ({ ...@@ -189,7 +207,7 @@ const ModelSelector: FC<Props> = ({
return ( return (
<Popover.Button <Popover.Button
key={`${model.providerName}-${model.value}`} key={`${model.providerName}-${model.value}`}
className={` className={`${s.optionItem}
flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50 flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50
${!readonly ? 'cursor-pointer' : 'cursor-auto'} ${!readonly ? 'cursor-pointer' : 'cursor-auto'}
${(value?.providerName === model.providerName && value?.modelName === model.value) && 'bg-gray-50'} ${(value?.providerName === model.providerName && value?.modelName === model.value) && 'bg-gray-50'}
...@@ -206,7 +224,12 @@ const ModelSelector: FC<Props> = ({ ...@@ -206,7 +224,12 @@ const ModelSelector: FC<Props> = ({
modelId={model.value} modelId={model.value}
providerName={model.providerName} providerName={model.providerName}
/> />
<div className='grow text-left text-sm text-gray-900 truncate'><ModelName modelId={model.value} modelDisplayName={model.modelDisplayName} /></div> <div className='mr-2 grow flex items-center text-left text-sm text-gray-900 truncate'>
<ModelName modelId={model.value} modelDisplayName={model.modelDisplayName} />
{isShowModelModeType && (
<ModelModeTypeLabel className={`${s.modelModeLabel} ml-2`} type={model.model_mode} />
)}
</div>
{ (value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' /> } { (value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' /> }
</Popover.Button> </Popover.Button>
) )
...@@ -218,10 +241,33 @@ const ModelSelector: FC<Props> = ({ ...@@ -218,10 +241,33 @@ const ModelSelector: FC<Props> = ({
{(search && filteredModelList.length === 0) && ( {(search && filteredModelList.length === 0) && (
<div className='px-3 pt-1.5 h-[30px] text-center text-xs text-gray-500'>{t('common.modelProvider.noModelFound', { model: search })}</div> <div className='px-3 pt-1.5 h-[30px] text-center text-xs text-gray-500'>{t('common.modelProvider.noModelFound', { model: search })}</div>
)} )}
{isShowAddModel && (
<div
className='border-t flex items-center h-9 pl-3 text-xs text-[#155EEF] cursor-pointer'
style={{
borderColor: 'rgba(0, 0, 0, 0.05)',
}}
onClick={() => {
setShowSettingModal(true)
}}
>
<CubeOutline className='w-4 h-4 mr-2' />
<div>{t('common.model.addMoreModel')}</div>
</div>
)}
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
)} )}
</Popover> </Popover>
{
showSettingModal && (
<AccountSetting activeTab="provider" onCancel={async () => {
setShowSettingModal(false)
}} />
)
}
</div> </div>
) )
} }
......
.modelModeLabel {
visibility: hidden;
}
.optionItem:hover .modelModeLabel {
visibility: visible;
}
\ No newline at end of file
...@@ -91,6 +91,20 @@ export const TONE_LIST = [ ...@@ -91,6 +91,20 @@ export const TONE_LIST = [
}, },
] ]
export const DEFAULT_CHAT_PROMPT_CONFIG = {
prompt: [],
}
export const DEFAULT_COMPLETION_PROMPT_CONFIG = {
prompt: {
text: '',
},
conversation_histories_role: {
user_prefix: '',
assistant_prefix: '',
},
}
export const getMaxToken = (modelId: string) => { export const getMaxToken = (modelId: string) => {
return (modelId === 'gpt-4' || modelId === 'gpt-3.5-turbo-16k') ? 8000 : 4000 return (modelId === 'gpt-4' || modelId === 'gpt-3.5-turbo-16k') ? 8000 : 4000
} }
...@@ -104,7 +118,7 @@ export const zhRegex = /^[\u4E00-\u9FA5]$/m ...@@ -104,7 +118,7 @@ export const zhRegex = /^[\u4E00-\u9FA5]$/m
export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/m export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/m
export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/m export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/m
const MAX_ZN_VAR_NAME_LENGHT = 8 const MAX_ZN_VAR_NAME_LENGHT = 8
const MAX_EN_VAR_VALUE_LENGHT = 16 const MAX_EN_VAR_VALUE_LENGHT = 30
export const getMaxVarNameLength = (value: string) => { export const getMaxVarNameLength = (value: string) => {
if (zhRegex.test(value)) if (zhRegex.test(value))
return MAX_ZN_VAR_NAME_LENGHT return MAX_ZN_VAR_NAME_LENGHT
...@@ -112,7 +126,9 @@ export const getMaxVarNameLength = (value: string) => { ...@@ -112,7 +126,9 @@ export const getMaxVarNameLength = (value: string) => {
return MAX_EN_VAR_VALUE_LENGHT return MAX_EN_VAR_VALUE_LENGHT
} }
export const MAX_VAR_KEY_LENGHT = 16 export const MAX_VAR_KEY_LENGHT = 30
export const MAX_PROMPT_MESSAGE_LENGTH = 10
export const VAR_ITEM_TEMPLATE = { export const VAR_ITEM_TEMPLATE = {
key: '', key: '',
......
import { createContext } from 'use-context-selector' import { createContext } from 'use-context-selector'
import type { CitationConfig, CompletionParams, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug' import { PromptMode } from '@/models/debug'
import type { BlockStatus, ChatPromptConfig, CitationConfig, CompletionParams, CompletionPromptConfig, ConversationHistoriesRole, DatasetConfigs, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptItem, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
import type { DataSet } from '@/models/datasets' import type { DataSet } from '@/models/datasets'
import { ModelModeType } from '@/types/app'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
type IDebugConfiguration = { type IDebugConfiguration = {
appId: string appId: string
hasSetAPIKEY: boolean hasSetAPIKEY: boolean
isTrailFinished: boolean isTrailFinished: boolean
mode: string mode: string
modelModeType: ModelModeType
promptMode: PromptMode
setPromptMode: (promptMode: PromptMode) => void
isAdvancedMode: boolean
canReturnToSimpleMode: boolean
setCanReturnToSimpleMode: (canReturnToSimpleMode: boolean) => void
chatPromptConfig: ChatPromptConfig
completionPromptConfig: CompletionPromptConfig
currentAdvancedPrompt: PromptItem | PromptItem[]
setCurrentAdvancedPrompt: (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => void
showHistoryModal: () => void
conversationHistoriesRole: ConversationHistoriesRole
setConversationHistoriesRole: (conversationHistoriesRole: ConversationHistoriesRole) => void
hasSetBlockStatus: BlockStatus
conversationId: string | null // after first chat send conversationId: string | null // after first chat send
setConversationId: (conversationId: string | null) => void setConversationId: (conversationId: string | null) => void
introduction: string introduction: string
...@@ -37,6 +54,10 @@ type IDebugConfiguration = { ...@@ -37,6 +54,10 @@ type IDebugConfiguration = {
setModelConfig: (modelConfig: ModelConfig) => void setModelConfig: (modelConfig: ModelConfig) => void
dataSets: DataSet[] dataSets: DataSet[]
setDataSets: (dataSet: DataSet[]) => void setDataSets: (dataSet: DataSet[]) => void
showSelectDataSet: () => void
// dataset config
datasetConfigs: DatasetConfigs
setDatasetConfigs: (config: DatasetConfigs) => void
hasSetContextVar: boolean hasSetContextVar: boolean
} }
...@@ -45,6 +66,27 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({ ...@@ -45,6 +66,27 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
hasSetAPIKEY: false, hasSetAPIKEY: false,
isTrailFinished: false, isTrailFinished: false,
mode: '', mode: '',
modelModeType: ModelModeType.chat,
promptMode: PromptMode.simple,
setPromptMode: () => { },
isAdvancedMode: false,
canReturnToSimpleMode: false,
setCanReturnToSimpleMode: () => { },
chatPromptConfig: DEFAULT_CHAT_PROMPT_CONFIG,
completionPromptConfig: DEFAULT_COMPLETION_PROMPT_CONFIG,
currentAdvancedPrompt: [],
showHistoryModal: () => { },
conversationHistoriesRole: {
user_prefix: 'user',
assistant_prefix: 'assistant',
},
setConversationHistoriesRole: () => { },
setCurrentAdvancedPrompt: () => { },
hasSetBlockStatus: {
context: false,
history: false,
query: false,
},
conversationId: '', conversationId: '',
setConversationId: () => { }, setConversationId: () => { },
introduction: '', introduction: '',
...@@ -89,6 +131,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({ ...@@ -89,6 +131,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
modelConfig: { modelConfig: {
provider: 'OPENAI', // 'OPENAI' provider: 'OPENAI', // 'OPENAI'
model_id: 'gpt-3.5-turbo', // 'gpt-3.5-turbo' model_id: 'gpt-3.5-turbo', // 'gpt-3.5-turbo'
mode: ModelModeType.unset,
configs: { configs: {
prompt_template: '', prompt_template: '',
prompt_variables: [], prompt_variables: [],
...@@ -102,7 +145,16 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({ ...@@ -102,7 +145,16 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
}, },
setModelConfig: () => { }, setModelConfig: () => { },
dataSets: [], dataSets: [],
showSelectDataSet: () => { },
setDataSets: () => { }, setDataSets: () => { },
datasetConfigs: {
top_k: 2,
score_threshold: {
enable: false,
value: 0.7,
},
},
setDatasetConfigs: () => {},
hasSetContextVar: false, hasSetContextVar: false,
}) })
......
const translation = { const translation = {
pageTitle: 'Prompt Engineering', pageTitle: {
line1: 'PROMPT',
line2: 'Engineering',
},
promptMode: {
simple: 'Switch to Advanced Mode to edit the built-in PROMPT',
advanced: 'Advanced Mode',
switchBack: 'Switch back',
advancedWarning: {
title: 'You have switched to Advanced Mode, and once you modify the PROMPT, you CANNOT return to the simple mode.',
description: 'In Advanced Mode, you can edit built-in PROMPT.',
learnMore: 'Learn more',
ok: 'OK',
},
operation: {
addMessage: 'Add Message',
},
},
operation: { operation: {
applyConfig: 'Publish', applyConfig: 'Publish',
resetConfig: 'Reset', resetConfig: 'Reset',
...@@ -80,6 +97,17 @@ const translation = { ...@@ -80,6 +97,17 @@ const translation = {
deleteContextVarTip: 'This variable has been set as a context query variable, and removing it will impact the normal use of the dataset. If you still need to delete it, please reselect it in the context section.', deleteContextVarTip: 'This variable has been set as a context query variable, and removing it will impact the normal use of the dataset. If you still need to delete it, please reselect it in the context section.',
}, },
}, },
conversationHistory: {
title: 'Conversation History',
description: 'Set prefix names for conversation roles',
tip: 'The Conversation History is not enabled, please add <histories> in the prompt above.',
learnMore: 'Learn more',
editModal: {
title: 'Edit Conversation Role Names',
userPrefix: 'User prefix',
assistantPrefix: 'Assistant prefix',
},
},
}, },
automatic: { automatic: {
title: 'Automated application orchestration', title: 'Automated application orchestration',
...@@ -136,13 +164,15 @@ const translation = { ...@@ -136,13 +164,15 @@ const translation = {
}, },
varKeyError: { varKeyError: {
canNoBeEmpty: 'Variable key can not be empty', canNoBeEmpty: 'Variable key can not be empty',
tooLong: 'Variable key: {{key}} too length. Can not be longer then 16 characters', tooLong: 'Variable key: {{key}} too length. Can not be longer then 30 characters',
notValid: 'Variable key: {{key}} is invalid. Can only contain letters, numbers, and underscores', notValid: 'Variable key: {{key}} is invalid. Can only contain letters, numbers, and underscores',
notStartWithNumber: 'Variable key: {{key}} can not start with a number', notStartWithNumber: 'Variable key: {{key}} can not start with a number',
keyAlreadyExists: 'Variable key: :{{key}} already exists', keyAlreadyExists: 'Variable key: :{{key}} already exists',
}, },
otherError: { otherError: {
promptNoBeEmpty: 'Prefix prompt can not be empty', promptNoBeEmpty: 'Prompt can not be empty',
historyNoBeEmpty: 'Conversation history must be set in the prompt',
queryNoBeEmpty: 'Query must be set in the prompt',
}, },
variableConig: { variableConig: {
modalTitle: 'Field settings', modalTitle: 'Field settings',
...@@ -172,9 +202,13 @@ const translation = { ...@@ -172,9 +202,13 @@ const translation = {
model: 'Model', model: 'Model',
setTone: 'Set tone of responses', setTone: 'Set tone of responses',
title: 'Model and Parameters', title: 'Model and Parameters',
modeType: {
chat: 'Chat',
completion: 'Complete',
},
}, },
inputs: { inputs: {
title: 'Debugging and Previewing', title: 'Debug and Preview',
noPrompt: 'Try write some prompt in pre-prompt input', noPrompt: 'Try write some prompt in pre-prompt input',
userInputField: 'User Input Field', userInputField: 'User Input Field',
noVar: 'Fill in the value of the variable, which will be automatically replaced in the prompt word every time a new session is started.', noVar: 'Fill in the value of the variable, which will be automatically replaced in the prompt word every time a new session is started.',
...@@ -188,6 +222,13 @@ const translation = { ...@@ -188,6 +222,13 @@ const translation = {
run: 'RUN', run: 'RUN',
}, },
result: 'Output Text', result: 'Output Text',
datasetConfig: {
params: 'Params',
top_k: 'Top K',
top_kTip: 'Used to filter segments that are most similar to user questions. The system will also dynamically adjust the value of Top K, according to max_tokens of the selected model.',
score_threshold: 'Score Threshold',
score_thresholdTip: 'Used to set the similarity threshold for segment filtering.',
},
} }
export default translation export default translation
const translation = { const translation = {
pageTitle: '提示词编排', pageTitle: {
line1: '提示词',
line2: '编排',
},
promptMode: {
simple: '切换到高级模式以编辑内置的提示词',
advanced: '高级模式',
switchBack: '返回简单模式',
advancedWarning: {
title: '您已切换到高级模式,一旦修改提示词,将无法返回简单模式。',
description: '在高级模式下,您可以编辑内置的提示词。',
learnMore: '了解更多',
ok: '确定',
},
operation: {
addMessage: '添加消息',
},
},
operation: { operation: {
applyConfig: '发布', applyConfig: '发布',
resetConfig: '重置', resetConfig: '重置',
...@@ -80,6 +97,17 @@ const translation = { ...@@ -80,6 +97,17 @@ const translation = {
deleteContextVarTip: '该变量已被设置为上下文查询变量,删除该变量将影响数据集的正常使用。 如果您仍需要删除它,请在上下文部分中重新选择它。', deleteContextVarTip: '该变量已被设置为上下文查询变量,删除该变量将影响数据集的正常使用。 如果您仍需要删除它,请在上下文部分中重新选择它。',
}, },
}, },
conversationHistory: {
title: '对话历史',
description: '设置对话角色的前缀名称',
tip: '对话历史未启用,请在上面的提示中添加<histories>。',
learnMore: '了解更多',
editModal: {
title: '编辑对话角色名称',
userPrefix: '用户前缀',
assistantPrefix: '助手前缀',
},
},
}, },
automatic: { automatic: {
title: '自动编排', title: '自动编排',
...@@ -132,13 +160,15 @@ const translation = { ...@@ -132,13 +160,15 @@ const translation = {
}, },
varKeyError: { varKeyError: {
canNoBeEmpty: '变量不能为空', canNoBeEmpty: '变量不能为空',
tooLong: '变量: {{key}} 长度太长。不能超过 16 个字符', tooLong: '变量: {{key}} 长度太长。不能超过 30 个字符',
notValid: '变量: {{key}} 非法。只能包含英文字符,数字和下划线', notValid: '变量: {{key}} 非法。只能包含英文字符,数字和下划线',
notStartWithNumber: '变量: {{key}} 不能以数字开头', notStartWithNumber: '变量: {{key}} 不能以数字开头',
keyAlreadyExists: '变量:{{key}} 已存在', keyAlreadyExists: '变量:{{key}} 已存在',
}, },
otherError: { otherError: {
promptNoBeEmpty: '前缀提示词不能为空', promptNoBeEmpty: '提示词不能为空',
historyNoBeEmpty: '提示词中必须设置对话历史',
queryNoBeEmpty: '提示词中必须设置查询内容',
}, },
variableConig: { variableConig: {
modalTitle: '变量设置', modalTitle: '变量设置',
...@@ -182,6 +212,13 @@ const translation = { ...@@ -182,6 +212,13 @@ const translation = {
run: '运行', run: '运行',
}, },
result: '结果', result: '结果',
datasetConfig: {
params: '参数设置',
top_k: 'Top K',
top_kTip: '用于筛选与用户问题相似度最高的文本片段。系统同时会根据选用模型上下文窗口大小动态调整分段数量。',
score_threshold: 'Score 阈值',
score_thresholdTip: '用于设置文本片段筛选的相似度阈值。',
},
} }
export default translation export default translation
...@@ -8,7 +8,7 @@ const translation = { ...@@ -8,7 +8,7 @@ const translation = {
endUser: 'End User', endUser: 'End User',
input: 'Input', input: 'Input',
output: 'Output', output: 'Output',
summary: 'Summary', summary: 'Title',
messageCount: 'Message Count', messageCount: 'Message Count',
userRate: 'User Rate', userRate: 'User Rate',
adminRate: 'Op. Rate', adminRate: 'Op. Rate',
...@@ -43,6 +43,7 @@ const translation = { ...@@ -43,6 +43,7 @@ const translation = {
editAnnotation: 'Edit Improvement', editAnnotation: 'Edit Improvement',
annotationPlaceholder: 'Enter the expected answer that you want AI to reply, which can be used for model fine-tuning and continuous improvement of text generation quality in the future.', annotationPlaceholder: 'Enter the expected answer that you want AI to reply, which can be used for model fine-tuning and continuous improvement of text generation quality in the future.',
}, },
variables: 'Variables',
}, },
filter: { filter: {
period: { period: {
......
...@@ -8,7 +8,7 @@ const translation = { ...@@ -8,7 +8,7 @@ const translation = {
endUser: '用户', endUser: '用户',
input: '输入', input: '输入',
output: '输出', output: '输出',
summary: '摘要', summary: '标题',
messageCount: '消息数', messageCount: '消息数',
userRate: '用户反馈', userRate: '用户反馈',
adminRate: '管理员反馈', adminRate: '管理员反馈',
...@@ -43,6 +43,7 @@ const translation = { ...@@ -43,6 +43,7 @@ const translation = {
editAnnotation: '编辑改进回复', editAnnotation: '编辑改进回复',
annotationPlaceholder: '输入你希望 AI 回复的预期答案,这在今后可用于模型微调,持续改进文本生成质量。', annotationPlaceholder: '输入你希望 AI 回复的预期答案,这在今后可用于模型微调,持续改进文本生成质量。',
}, },
variables: '变量',
}, },
filter: { filter: {
period: { period: {
......
...@@ -29,6 +29,7 @@ const translation = { ...@@ -29,6 +29,7 @@ const translation = {
getForFree: 'Get for free', getForFree: 'Get for free',
reload: 'Reload', reload: 'Reload',
ok: 'OK', ok: 'OK',
log: 'Log',
}, },
placeholder: { placeholder: {
input: 'Please enter', input: 'Please enter',
...@@ -66,6 +67,9 @@ const translation = { ...@@ -66,6 +67,9 @@ const translation = {
'Used to limit the maximum length of the reply, in tokens. \nLarger values may limit the space left for prompt words, chat logs, and data sets. \nIt is recommended to set it below two-thirds.', 'Used to limit the maximum length of the reply, in tokens. \nLarger values may limit the space left for prompt words, chat logs, and data sets. \nIt is recommended to set it below two-thirds.',
maxTokenSettingTip: 'Your max token setting is high, potentially limiting space for prompts, queries, and data. Consider setting it below 2/3.', maxTokenSettingTip: 'Your max token setting is high, potentially limiting space for prompts, queries, and data. Consider setting it below 2/3.',
setToCurrentModelMaxTokenTip: 'Max token is updated to the 80% maximum token of the current model {{maxToken}}.', setToCurrentModelMaxTokenTip: 'Max token is updated to the 80% maximum token of the current model {{maxToken}}.',
stop_sequences: 'Stop sequences',
stop_sequencesTip: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.',
stop_sequencesPlaceholder: 'Enter sequence and press Tab',
}, },
tone: { tone: {
Creative: 'Creative', Creative: 'Creative',
...@@ -73,6 +77,7 @@ const translation = { ...@@ -73,6 +77,7 @@ const translation = {
Precise: 'Precise', Precise: 'Precise',
Custom: 'Custom', Custom: 'Custom',
}, },
addMoreModel: 'Go to settings to add more models',
}, },
menus: { menus: {
status: 'beta', status: 'beta',
...@@ -352,6 +357,48 @@ const translation = { ...@@ -352,6 +357,48 @@ const translation = {
hitScore: 'Hit Score:', hitScore: 'Hit Score:',
}, },
}, },
promptEditor: {
placeholder: 'Write your prompt word here, enter \'{\' to insert a variable, enter \'/\' to insert a prompt content block',
context: {
item: {
title: 'Context',
desc: 'Insert context template',
},
modal: {
title: '{{num}} Datasets in Context',
add: 'Add Context ',
footer: 'You can manage contexts in the Context section below.',
},
},
history: {
item: {
title: 'Conversation History',
desc: 'Insert historical message template',
},
modal: {
title: 'EXAMPLE',
user: 'Hello',
assistant: 'Hello! How can I assist you today?',
edit: 'Edit Conversation Role Names',
},
},
variable: {
item: {
title: 'Variables',
desc: 'Insert variable template',
},
modal: {
add: 'New variable',
},
},
query: {
item: {
title: 'Query',
desc: 'Insert user query template',
},
},
existed: 'Already exists in the prompt',
},
} }
export default translation export default translation
...@@ -29,6 +29,7 @@ const translation = { ...@@ -29,6 +29,7 @@ const translation = {
getForFree: '免费获取', getForFree: '免费获取',
reload: '刷新', reload: '刷新',
ok: '好的', ok: '好的',
log: '日志',
}, },
placeholder: { placeholder: {
input: '请输入', input: '请输入',
...@@ -66,6 +67,9 @@ const translation = { ...@@ -66,6 +67,9 @@ const translation = {
'用于限制回复的最大长度,以 token 为单位。\n较大的值可能会限制给提示词、聊天记录和数据集留出的空间。\n建议将其设置在三分之二以下。', '用于限制回复的最大长度,以 token 为单位。\n较大的值可能会限制给提示词、聊天记录和数据集留出的空间。\n建议将其设置在三分之二以下。',
maxTokenSettingTip: '您设置的最大 tokens 数较大,可能会导致 prompt、用户问题、数据集内容没有 token 空间进行处理,建议设置到 2/3 以下。', maxTokenSettingTip: '您设置的最大 tokens 数较大,可能会导致 prompt、用户问题、数据集内容没有 token 空间进行处理,建议设置到 2/3 以下。',
setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 {{maxToken}} 的 80%。', setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 {{maxToken}} 的 80%。',
stop_sequences: '停止序列 stop_sequences',
stop_sequencesTip: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。',
stop_sequencesPlaceholder: '输入序列并按 Tab 键',
}, },
tone: { tone: {
Creative: '创意', Creative: '创意',
...@@ -73,6 +77,7 @@ const translation = { ...@@ -73,6 +77,7 @@ const translation = {
Precise: '精确', Precise: '精确',
Custom: '自定义', Custom: '自定义',
}, },
addMoreModel: '添加更多模型',
}, },
menus: { menus: {
status: 'beta', status: 'beta',
...@@ -352,6 +357,48 @@ const translation = { ...@@ -352,6 +357,48 @@ const translation = {
hitScore: '命中得分:', hitScore: '命中得分:',
}, },
}, },
promptEditor: {
placeholder: '在这里写你的提示词,输入\'{\' 插入变量、输入\'/\' 插入提示内容块',
context: {
item: {
title: '上下文',
desc: '插入上下文模板',
},
modal: {
title: '有 {{num}} 个数据集在上下文中',
add: '添加上下文',
footer: '您可以在下面的“上下文”部分中管理上下文。',
},
},
history: {
item: {
title: '会话历史',
desc: '插入历史消息模板',
},
modal: {
title: '示例',
user: '你好',
assistant: '你好!今天我能为您提供什么帮助?',
edit: '编辑对话角色名称',
},
},
variable: {
item: {
title: '变量',
desc: '插入变量模板',
},
modal: {
add: '添加新变量',
},
},
query: {
item: {
title: '查询内容',
desc: '插入用户查询模板',
},
},
existed: 'Prompt 中已存在',
},
} }
export default translation export default translation
import type { ModelModeType } from '@/types/app'
export type Inputs = Record<string, string | number | object> export type Inputs = Record<string, string | number | object>
export enum PromptMode {
simple = 'simple',
advanced = 'advanced',
}
export type PromptItem = {
role?: PromptRole
text: string
}
export type ChatPromptConfig = {
prompt: PromptItem[]
}
export type ConversationHistoriesRole = {
user_prefix: string
assistant_prefix: string
}
export type CompletionPromptConfig = {
prompt: PromptItem
conversation_histories_role: ConversationHistoriesRole
}
export type BlockStatus = {
context: boolean
history: boolean
query: boolean
}
export enum PromptRole {
system = 'system',
user = 'user',
assistant = 'assistant',
}
export type PromptVariable = { export type PromptVariable = {
key: string key: string
name: string name: string
...@@ -17,6 +53,7 @@ export type CompletionParams = { ...@@ -17,6 +53,7 @@ export type CompletionParams = {
top_p: number top_p: number
presence_penalty: number presence_penalty: number
frequency_penalty: number frequency_penalty: number
stop?: string[]
} }
export type ModelId = 'gpt-3.5-turbo' | 'text-davinci-003' export type ModelId = 'gpt-3.5-turbo' | 'text-davinci-003'
...@@ -42,6 +79,7 @@ export type RetrieverResourceConfig = MoreLikeThisConfig ...@@ -42,6 +79,7 @@ export type RetrieverResourceConfig = MoreLikeThisConfig
export type ModelConfig = { export type ModelConfig = {
provider: string // LLM Provider: for example "OPENAI" provider: string // LLM Provider: for example "OPENAI"
model_id: string model_id: string
mode: ModelModeType
configs: PromptConfig configs: PromptConfig
opening_statement: string | null opening_statement: string | null
more_like_this: MoreLikeThisConfig | null more_like_this: MoreLikeThisConfig | null
...@@ -50,6 +88,14 @@ export type ModelConfig = { ...@@ -50,6 +88,14 @@ export type ModelConfig = {
retriever_resource: RetrieverResourceConfig | null retriever_resource: RetrieverResourceConfig | null
dataSets: any[] dataSets: any[]
} }
export type DatasetConfigItem = {
enable: boolean
value: number
}
export type DatasetConfigs = {
top_k: number
score_threshold: DatasetConfigItem
}
export type DebugRequestBody = { export type DebugRequestBody = {
inputs: Inputs inputs: Inputs
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"@formatjs/intl-localematcher": "^0.2.32", "@formatjs/intl-localematcher": "^0.2.32",
"@headlessui/react": "^1.7.13", "@headlessui/react": "^1.7.13",
"@heroicons/react": "^2.0.16", "@heroicons/react": "^2.0.16",
"@lexical/react": "^0.12.2",
"@mdx-js/loader": "^2.3.0", "@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0", "@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.2.4", "@next/mdx": "^13.2.4",
...@@ -42,6 +43,7 @@ ...@@ -42,6 +43,7 @@
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"katex": "^0.16.7", "katex": "^0.16.7",
"lamejs": "1.2.0", "lamejs": "1.2.0",
"lexical": "^0.12.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mermaid": "10.4.0", "mermaid": "10.4.0",
"negotiator": "^0.6.3", "negotiator": "^0.6.3",
......
import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd } from './base' import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd } from './base'
import { get, post, ssePost } from './base' import { get, post, ssePost } from './base'
import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug'
import type { ModelModeType } from '@/types/app'
export type AutomaticRes = { export type AutomaticRes = {
prompt: string prompt: string
...@@ -64,3 +66,26 @@ export const fetchModelParams = (providerName: string, modelId: string) => { ...@@ -64,3 +66,26 @@ export const fetchModelParams = (providerName: string, modelId: string) => {
}, },
}) })
} }
export const fetchPromptTemplate = ({
appMode,
mode,
modelName,
hasSetDataSet,
}: { appMode: string; mode: ModelModeType; modelName: string; hasSetDataSet: boolean }) => {
return get<Promise<{ chat_prompt_config: ChatPromptConfig; completion_prompt_config: CompletionPromptConfig }>>('/app/prompt-templates', {
params: {
app_mode: appMode,
model_mode: mode,
model_name: modelName,
has_context: hasSetDataSet,
},
})
}
export const fetchTextGenerationMessge = ({
appId,
messageId,
}: { appId: string; messageId: string }) => {
return get<Promise<{ message: [] }>>(`/apps/${appId}/messages/${messageId}`)
}
...@@ -51,7 +51,9 @@ module.exports = { ...@@ -51,7 +51,9 @@ module.exports = {
indigo: { indigo: {
25: '#F5F8FF', 25: '#F5F8FF',
100: '#E0EAFF', 100: '#E0EAFF',
400: '#8098F9',
600: '#444CE7', 600: '#444CE7',
800: '#2D31A6',
}, },
}, },
screens: { screens: {
......
import type { ChatPromptConfig, CompletionPromptConfig, DatasetConfigs, PromptMode } from '@/models/debug.ts'
export enum ProviderType { export enum ProviderType {
openai = 'openai', openai = 'openai',
anthropic = 'anthropic', anthropic = 'anthropic',
...@@ -14,6 +15,12 @@ export enum AppType { ...@@ -14,6 +15,12 @@ export enum AppType {
'completion' = 'completion', 'completion' = 'completion',
} }
export enum ModelModeType {
'chat' = 'chat',
'completion' = 'completion',
'unset' = '',
}
export type VariableInput = { export type VariableInput = {
key: string key: string
name: string name: string
...@@ -89,6 +96,9 @@ export type ToolItem = { ...@@ -89,6 +96,9 @@ export type ToolItem = {
export type ModelConfig = { export type ModelConfig = {
opening_statement: string opening_statement: string
pre_prompt: string pre_prompt: string
prompt_type: PromptMode
chat_prompt_config: ChatPromptConfig | {}
completion_prompt_config: CompletionPromptConfig | {}
user_input_form: UserInputFormItem[] user_input_form: UserInputFormItem[]
dataset_query_variable?: string dataset_query_variable?: string
more_like_this: { more_like_this: {
...@@ -112,6 +122,7 @@ export type ModelConfig = { ...@@ -112,6 +122,7 @@ export type ModelConfig = {
provider: string provider: string
/** Model name, e.g, gpt-3.5.turbo */ /** Model name, e.g, gpt-3.5.turbo */
name: string name: string
mode: ModelModeType
/** Default Completion call parameters */ /** Default Completion call parameters */
completion_params: { completion_params: {
/** Maximum number of tokens in the answer message returned by Completion */ /** Maximum number of tokens in the answer message returned by Completion */
...@@ -159,6 +170,7 @@ export type ModelConfig = { ...@@ -159,6 +170,7 @@ export type ModelConfig = {
frequency_penalty: number frequency_penalty: number
} }
} }
dataset_configs: DatasetConfigs
} }
export const LanguagesSupported = ['zh-Hans', 'en-US'] as const export const LanguagesSupported = ['zh-Hans', 'en-US'] as const
......
import { MAX_VAR_KEY_LENGHT, VAR_ITEM_TEMPLATE, getMaxVarNameLength } from '@/config' import { MAX_VAR_KEY_LENGHT, VAR_ITEM_TEMPLATE, getMaxVarNameLength } from '@/config'
import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT } from '@/app/components/base/prompt-editor/constants'
const otherAllowedRegex = /^[a-zA-Z0-9_]+$/ const otherAllowedRegex = /^[a-zA-Z0-9_]+$/
export const getNewVar = (key: string) => { export const getNewVar = (key: string) => {
return { return {
...VAR_ITEM_TEMPLATE, ...VAR_ITEM_TEMPLATE,
...@@ -45,3 +45,23 @@ export const checkKeys = (keys: string[], canBeEmpty?: boolean) => { ...@@ -45,3 +45,23 @@ export const checkKeys = (keys: string[], canBeEmpty?: boolean) => {
}) })
return { isValid, errorKey, errorMessageKey } return { isValid, errorKey, errorMessageKey }
} }
const varRegex = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g
export const getVars = (value: string) => {
const keys = value.match(varRegex)?.filter((item) => {
return ![CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT].includes(item)
}).map((item) => {
return item.replace('{{', '').replace('}}', '')
}).filter(key => key.length <= MAX_VAR_KEY_LENGHT) || []
const keyObj: Record<string, boolean> = {}
// remove duplicate keys
const res: string[] = []
keys.forEach((key) => {
if (keyObj[key])
return
keyObj[key] = true
res.push(key)
})
return res
}
...@@ -260,6 +260,160 @@ ...@@ -260,6 +260,160 @@
"@jridgewell/resolve-uri" "3.1.0" "@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14" "@jridgewell/sourcemap-codec" "1.4.14"
"@lexical/clipboard@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/clipboard/-/clipboard-0.12.2.tgz#cfa74da178673624ecd835eb9449545885cbefc7"
integrity sha512-RldmfZquuJJJCJ5WquCyoJ1/eZ+AnNgdksqvd+G+Yn/GyJl/+O3dnHM0QVaDSPvh/PynLFcCtz/57ySLo2kQxQ==
dependencies:
"@lexical/html" "0.12.2"
"@lexical/list" "0.12.2"
"@lexical/selection" "0.12.2"
"@lexical/utils" "0.12.2"
"@lexical/code@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/code/-/code-0.12.2.tgz#2484511cb9c3688bb85f477d448444250939986d"
integrity sha512-w2JeJdnMUtYnC/Fx78sL3iJBt9Ug8pFSDOcI9ay/BkMQFQV8oqq1iyuLLBBJSG4FAM8b2DXrVdGklRQ+jTfTVw==
dependencies:
"@lexical/utils" "0.12.2"
prismjs "^1.27.0"
"@lexical/dragon@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/dragon/-/dragon-0.12.2.tgz#0ae209449a7ebe57c078584e28edb1b7354ecf99"
integrity sha512-Mt8NLzTOt+VgQtc2DKDbHBwKeRlvKqbLqRIMYUVk60gol+YV7NpVBsP1PAMuYYjrTQLhlckBSC32H1SUHZRavA==
"@lexical/hashtag@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/hashtag/-/hashtag-0.12.2.tgz#d9c0f872b4a662b0393b289fa84515ec2c7cd624"
integrity sha512-2vYzIu5Ldf+eYdUrNA2m80c3N3MF3vJ0fIJzpl5QyX8OdViggEWl1bh+lKtw1Ju0H0CUyDIXdDLZ2apW3WDkTA==
dependencies:
"@lexical/utils" "0.12.2"
"@lexical/history@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/history/-/history-0.12.2.tgz#6870871c8120468a213e47c5b303f5b1dd378cba"
integrity sha512-PM/EDjnUyBPMWh1UiYb7T+FLbvTk14HwUWLXvZxn72S6Kj8ExH/PfLbWZWLCFL8RfzvbP407VwfSN8S0bF5H6g==
dependencies:
"@lexical/utils" "0.12.2"
"@lexical/html@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/html/-/html-0.12.2.tgz#6df09c362688b666da0f474d132d889415dbae8a"
integrity sha512-LWUO6OKhDtDZa9X1spHAqzsp+4EF01exis4cz5H9y2sHi7EofogXnRCadZ+fa07NVwPVTZWsStkk5qdSe/NEzg==
dependencies:
"@lexical/selection" "0.12.2"
"@lexical/link@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/link/-/link-0.12.2.tgz#33bf3ecdd98c2b27b2bdd67a8885649e1f528fc2"
integrity sha512-etOIONa7uyRDmwg8GN52kDlf8thD2Zk1LOFLeocHWz1V8fe3i2unGUek5s/rNPkc6ynpPpNsHdN1VEghOLCCmw==
dependencies:
"@lexical/utils" "0.12.2"
"@lexical/list@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/list/-/list-0.12.2.tgz#7afdd13026f936144ff93d40382f21924e8e6908"
integrity sha512-3CyWtYQC+IlK4cK/oiD8Uz1gSXD8UcKGOF2vVsDXkMU06O6zvHNmHZOnVJqA0JVNgZAoR9dMR1fi2xd4iuCAiw==
dependencies:
"@lexical/utils" "0.12.2"
"@lexical/mark@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/mark/-/mark-0.12.2.tgz#bfcfffc9f74505e5a6160679812b3d6bafea9c78"
integrity sha512-ub+37PDfmThsqAWipRTrwqpgE+83ckqJ5C3mKQUBZvhZfVZW1rEUXZnKjFh2Q3eZK6iT7zVgoVJWJS9ZgEEyag==
dependencies:
"@lexical/utils" "0.12.2"
"@lexical/markdown@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/markdown/-/markdown-0.12.2.tgz#b11559590e8684525d4a11d2d78108c3498896b9"
integrity sha512-F2jTFtBp7Q+yoA11BeUOEcxhROzW+HUhUGdsn20pSLhuxsWRj3oUuryWFeNKFofpzTCVoqU6dwpaMNMI2mL/sQ==
dependencies:
"@lexical/code" "0.12.2"
"@lexical/link" "0.12.2"
"@lexical/list" "0.12.2"
"@lexical/rich-text" "0.12.2"
"@lexical/text" "0.12.2"
"@lexical/utils" "0.12.2"
"@lexical/offset@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/offset/-/offset-0.12.2.tgz#9e4a36508790de1d643d4328986c5c3a2d847de0"
integrity sha512-rZLZXfOBmpmM8A2UZsX3cr/CQYw5F/ou67AbaKI0WImb5sjnIgICZqzu9VFUnkKlVNUurEpplV3UG3D1YYh1OQ==
"@lexical/overflow@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/overflow/-/overflow-0.12.2.tgz#b21ba828d6f02e99456d05df6c1d2771fb82aceb"
integrity sha512-UgE5j3ukO6qRFRpH4T7m/DvnodE9nCtImD7QinyGdsTa0hi5xlRnl0FUo605vH+vz7xEsUNAGwQXYPX9Sc/vig==
"@lexical/plain-text@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/plain-text/-/plain-text-0.12.2.tgz#87c51de239190d9202122a62aeaae04cb2477cda"
integrity sha512-Lcg6+ngRnX70//kz34azYhID3bvW66HSHCfu5UPhCXT+vQ/Jkd/InhRKajBwWXpaJxMM1huoi3sjzVDb3luNtw==
"@lexical/react@^0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/react/-/react-0.12.2.tgz#336805af7a3d8e69d5b0ef3d0efd7bd4cd8f29fb"
integrity sha512-ZBUvf5xmhiYWBw8pPrhYmLAEwFWrbF/cd15y76TUKD9l/2zDwwPs6nJQxBzfz3ei65r2/nnavLDV8W3QfvxfUA==
dependencies:
"@lexical/clipboard" "0.12.2"
"@lexical/code" "0.12.2"
"@lexical/dragon" "0.12.2"
"@lexical/hashtag" "0.12.2"
"@lexical/history" "0.12.2"
"@lexical/link" "0.12.2"
"@lexical/list" "0.12.2"
"@lexical/mark" "0.12.2"
"@lexical/markdown" "0.12.2"
"@lexical/overflow" "0.12.2"
"@lexical/plain-text" "0.12.2"
"@lexical/rich-text" "0.12.2"
"@lexical/selection" "0.12.2"
"@lexical/table" "0.12.2"
"@lexical/text" "0.12.2"
"@lexical/utils" "0.12.2"
"@lexical/yjs" "0.12.2"
react-error-boundary "^3.1.4"
"@lexical/rich-text@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/rich-text/-/rich-text-0.12.2.tgz#9ef44e54777fc222ac8df344adb0d7428c784b25"
integrity sha512-igsEuv7CwBOAj5c8jeE41cnx6zkhI/Bkbu4W7shT6S6lNA/3cnyZpAMlgixwyK5RoqjGRCT+IJK5l6yBxQfNkw==
"@lexical/selection@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/selection/-/selection-0.12.2.tgz#87e30b953b02923d1691f61d5189eb1be81a19ae"
integrity sha512-h+g3oOnihHKIyLTyG6uLCEVR/DmUEVdCcZO1iAoGsuW7nwWiWNPWj6oZ3Cw5J1Mk5u62DHnkkVDQsVSZbAwmtg==
"@lexical/table@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/table/-/table-0.12.2.tgz#59be2fd0413a9125017bb119aa950050bf92717b"
integrity sha512-tiAmTq6RKHDVER9v589Ajm9/RL+WTF1WschrH6HHVCtil6cfJfTJeJ+MF45+XEzB9fkqy2LfrScAfWxqLjVePA==
dependencies:
"@lexical/utils" "0.12.2"
"@lexical/text@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/text/-/text-0.12.2.tgz#623f8a9a56606e202febf4af80cfb5cf2da2a530"
integrity sha512-HyuIGuQvVi5djJKKBf+jYEBjK+0Eo9cKHf6WS7dlFozuCZvcCQEJkFy2yceWOwIVk+f2kptVQ5uO7aiZHExH2A==
"@lexical/utils@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/utils/-/utils-0.12.2.tgz#b8325f1a1a0d043fd2b40d59cd00c4d7c038474b"
integrity sha512-xW4y4l2Yd37+qLwkBvBGyzsKCA9wnh1ljphBJeR2vreT193i2gaIwuku2ZKlER14VHw4192qNJF7vUoAEmwurQ==
dependencies:
"@lexical/list" "0.12.2"
"@lexical/selection" "0.12.2"
"@lexical/table" "0.12.2"
"@lexical/yjs@0.12.2":
version "0.12.2"
resolved "https://registry.npmmirror.com/@lexical/yjs/-/yjs-0.12.2.tgz#c0bebe1795ecb7e68d88d255dcbfcaa02740539d"
integrity sha512-OPJhkJD1Mp9W80mfLzASTB3OFWFMzJteUYA+eSyDgiX9zNi1VGxAqmIITTkDvnCMa+qvw4EfhGeGezpjx6Og4A==
dependencies:
"@lexical/offset" "0.12.2"
"@mdx-js/loader@^2.3.0": "@mdx-js/loader@^2.3.0":
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/@mdx-js/loader/-/loader-2.3.0.tgz" resolved "https://registry.npmjs.org/@mdx-js/loader/-/loader-2.3.0.tgz"
...@@ -3478,6 +3632,11 @@ levn@^0.4.1: ...@@ -3478,6 +3632,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1" prelude-ls "^1.2.1"
type-check "~0.4.0" type-check "~0.4.0"
lexical@^0.12.2:
version "0.12.2"
resolved "https://registry.npmmirror.com/lexical/-/lexical-0.12.2.tgz#68535e145715d5788374495c9ee0420e417d792b"
integrity sha512-Kxavd+ETjxtVwG/hvPd6WZfXD44sLOKe9Vlkwxy7lBQ1qZArS+rZfs+u5iXwXe6tX9f2PIM0u3RHsrCEDDE0fw==
lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0: lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"
...@@ -4978,6 +5137,13 @@ react-dom@^18.2.0: ...@@ -4978,6 +5137,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-error-boundary@^3.1.4:
version "3.1.4"
resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-boundary@^4.0.2: react-error-boundary@^4.0.2:
version "4.0.9" version "4.0.9"
resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.9.tgz" resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.9.tgz"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment