Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
zxhlyh
2024-10-21 10:32:37 +08:00
committed by GitHub
parent 4fd2743efa
commit 7a1d6fe509
445 changed files with 11759 additions and 6922 deletions

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Tooltip from '@/app/components/base/tooltip'
export const Item: FC<{ title: string; tooltip: string; children: JSX.Element }> = ({
title,
tooltip,
children,
}) => {
return (
<div>
<div className='flex items-center space-x-1'>
<div>{title}</div>
<Tooltip
popupContent={
<div className='max-w-[200px] leading-[18px] text-[13px] font-medium text-gray-800'>{tooltip}</div>
}
/>
</div>
<div>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,152 @@
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePathname, useRouter } from 'next/navigation'
import produce from 'immer'
import { RiEqualizer2Line, RiExternalLinkLine } from '@remixicon/react'
import { MessageFast } from '@/app/components/base/icons/src/vender/features'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import useAnnotationConfig from '@/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { ANNOTATION_DEFAULT } from '@/config'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
}
const AnnotationReply = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const featuresStore = useFeaturesStore()
const annotationReply = useFeatures(s => s.features.annotationReply)
const updateAnnotationReply = useCallback((newConfig: any) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.annotationReply = newConfig
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
}, [featuresStore, onChange])
const {
handleEnableAnnotation,
handleDisableAnnotation,
isShowAnnotationConfigInit,
setIsShowAnnotationConfigInit,
isShowAnnotationFullModal,
setIsShowAnnotationFullModal,
} = useAnnotationConfig({
appId,
annotationConfig: annotationReply as any || {
id: '',
enabled: false,
score_threshold: ANNOTATION_DEFAULT.score_threshold,
embedding_model: {
embedding_provider_name: '',
embedding_model_name: '',
},
},
setAnnotationConfig: updateAnnotationReply,
})
const handleSwitch = useCallback((enabled: boolean) => {
if (enabled)
setIsShowAnnotationConfigInit(true)
else
handleDisableAnnotation(annotationReply?.embedding_model as any)
}, [annotationReply?.embedding_model, handleDisableAnnotation, setIsShowAnnotationConfigInit])
const [isHovering, setIsHovering] = useState(false)
return (
<>
<FeatureCard
icon={
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
<MessageFast className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.annotation.title')}
value={!!annotationReply?.enabled}
onChange={state => handleSwitch(state)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<>
{!annotationReply?.enabled && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.annotation.description')}</div>
)}
{!!annotationReply?.enabled && (
<>
{!isHovering && (
<div className='pt-0.5 flex items-center gap-4'>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.annotation.scoreThreshold.title')}</div>
<div className='text-text-secondary system-xs-regular'>{annotationReply.score_threshold || '-'}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('common.modelProvider.embeddingModel.key')}</div>
<div className='text-text-secondary system-xs-regular'>{annotationReply.embedding_model?.embedding_model_name}</div>
</div>
</div>
)}
{isHovering && (
<div className='flex items-center justify-between'>
<Button className='w-[178px]' onClick={() => setIsShowAnnotationConfigInit(true)} disabled={disabled}>
<RiEqualizer2Line className='mr-1 w-4 h-4' />
{t('common.operation.params')}
</Button>
<Button className='w-[178px]' onClick={() => {
router.push(`/app/${appId}/annotations`)
}}>
<RiExternalLinkLine className='mr-1 w-4 h-4' />
{t('appDebug.feature.annotation.cacheManagement')}
</Button>
</div>
)}
</>
)}
</>
</FeatureCard>
<ConfigParamModal
appId={appId}
isInit
isShow={isShowAnnotationConfigInit}
onHide={() => {
setIsShowAnnotationConfigInit(false)
// showChooseFeatureTrue()
}}
onSave={async (embeddingModel, score) => {
await handleEnableAnnotation(embeddingModel, score)
setIsShowAnnotationConfigInit(false)
}}
annotationConfig={annotationReply as any}
/>
{isShowAnnotationFullModal && (
<AnnotationFullModal
show={isShowAnnotationFullModal}
onHide={() => setIsShowAnnotationFullModal(false)}
/>
)}
</>
)
}
export default AnnotationReply

View File

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

View File

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

View File

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

View File

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

View File

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