mirror of
https://github.com/langgenius/dify.git
synced 2026-03-28 17:40:53 +08:00
use base ui toast
This commit is contained in:
@ -64,7 +64,7 @@ export const useUpdateAccessMode = () => {
|
||||
|
||||
// Component only adds UI behavior.
|
||||
updateAccessMode({ appId, mode }, {
|
||||
onSuccess: () => Toast.notify({ type: 'success', message: '...' }),
|
||||
onSuccess: () => toast.success('...'),
|
||||
})
|
||||
|
||||
// Avoid putting invalidation knowledge in the component.
|
||||
@ -114,10 +114,7 @@ try {
|
||||
router.push(`/orders/${order.id}`)
|
||||
}
|
||||
catch (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
toast.error(error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Preview } from '@storybook/react'
|
||||
import type { Resource } from 'i18next'
|
||||
import { withThemeByDataAttribute } from '@storybook/addon-themes'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ToastProvider } from '../app/components/base/toast'
|
||||
import { ToastHost } from '../app/components/base/ui/toast'
|
||||
import { I18nClientProvider as I18N } from '../app/components/provider/i18n'
|
||||
import commonEnUS from '../i18n/en-US/common.json'
|
||||
|
||||
@ -39,9 +39,10 @@ export const decorators = [
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18N locale="en-US" resource={storyResources}>
|
||||
<ToastProvider>
|
||||
<>
|
||||
<ToastHost />
|
||||
<Story />
|
||||
</ToastProvider>
|
||||
</>
|
||||
</I18N>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
@ -25,6 +25,19 @@ let mockSystemFeatures = {
|
||||
|
||||
const mockRouterPush = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const mockOnPlanInfoChanged = vi.fn()
|
||||
const mockDeleteAppMutation = vi.fn().mockResolvedValue(undefined)
|
||||
let mockDeleteMutationPending = false
|
||||
@ -94,27 +107,6 @@ vi.mock('@/context/provider-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the ToastContext used via useContext from use-context-selector
|
||||
vi.mock('use-context-selector', async () => {
|
||||
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: () => ({ notify: mockNotify }),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/tag-management/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
tagList: [],
|
||||
showTagManagementModal: false,
|
||||
setTagList: vi.fn(),
|
||||
setShowTagManagementModal: vi.fn(),
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/tag', () => ({
|
||||
fetchTagList: vi.fn().mockResolvedValue([]),
|
||||
}))
|
||||
|
||||
@ -33,7 +33,7 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useInvalidDatasetList: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
|
||||
@ -10,6 +10,19 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
const mockDoSyncWorkflowDraft = vi.fn().mockResolvedValue(undefined)
|
||||
const mockExportPipelineConfig = vi.fn().mockResolvedValue({ data: 'yaml-content' })
|
||||
const mockNotify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const mockEventEmitter = { emit: vi.fn() }
|
||||
const mockDownloadBlob = vi.fn()
|
||||
|
||||
@ -19,10 +32,6 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
}))
|
||||
|
||||
@ -153,7 +153,7 @@ vi.mock('@/app/components/base/confirm', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
|
||||
@ -8,12 +8,11 @@ import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/app-card'
|
||||
import TriggerCard from '@/app/components/app/overview/trigger-card'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
@ -37,7 +36,6 @@ export type ICardViewProps = {
|
||||
|
||||
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
|
||||
@ -106,10 +104,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
}
|
||||
}
|
||||
|
||||
notify({
|
||||
type,
|
||||
message: t(`actionMsg.${message}`, { ns: 'common' }) as string,
|
||||
})
|
||||
toast(t(`actionMsg.${message}`, { ns: 'common' }) as string, { type })
|
||||
}
|
||||
|
||||
// Listen for collaborative app state updates from other clients
|
||||
|
||||
@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
@ -43,10 +43,7 @@ const Panel: FC = () => {
|
||||
await updateTracingStatus({ appId, body: tracingStatus })
|
||||
setTracingStatus(tracingStatus)
|
||||
if (!noToast) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
toast(t('api.success', { ns: 'common' }), { type: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
|
||||
import { docURL } from './config'
|
||||
import Field from './field'
|
||||
@ -155,10 +155,7 @@ const ProviderConfigModal: FC<Props> = ({
|
||||
appId,
|
||||
provider: type,
|
||||
})
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('api.remove', { ns: 'common' }),
|
||||
})
|
||||
toast(t('api.remove', { ns: 'common' }), { type: 'success' })
|
||||
onRemoved()
|
||||
hideRemoveConfirm()
|
||||
}, [hideRemoveConfirm, appId, type, t, onRemoved])
|
||||
@ -264,10 +261,7 @@ const ProviderConfigModal: FC<Props> = ({
|
||||
return
|
||||
const errorMessage = checkValid()
|
||||
if (errorMessage) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
toast(errorMessage, { type: 'error' })
|
||||
return
|
||||
}
|
||||
const action = isEdit ? updateTracingConfig : addTracingConfig
|
||||
@ -279,10 +273,7 @@ const ProviderConfigModal: FC<Props> = ({
|
||||
tracing_config: config,
|
||||
},
|
||||
})
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
toast(t('api.success', { ns: 'common' }), { type: 'success' })
|
||||
onSaved(config)
|
||||
if (isAdd)
|
||||
onChosen(type)
|
||||
|
||||
@ -8,15 +8,14 @@ import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
|
||||
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
|
||||
@ -25,7 +24,6 @@ type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
|
||||
|
||||
const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
|
||||
const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
|
||||
@ -48,24 +46,24 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
|
||||
setIsShowAvatarPicker(false)
|
||||
onSave?.()
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
toast.error((e as Error).message)
|
||||
}
|
||||
}, [notify, onSave, t])
|
||||
}, [onSave, t])
|
||||
|
||||
const handleDeleteAvatar = useCallback(async () => {
|
||||
try {
|
||||
await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
setIsShowDeleteConfirm(false)
|
||||
onSave?.()
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
toast.error((e as Error).message)
|
||||
}
|
||||
}, [notify, onSave, t])
|
||||
}, [onSave, t])
|
||||
|
||||
const { handleLocalFileUpload } = useLocalFileUploader({
|
||||
limit: 3,
|
||||
@ -134,45 +132,39 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
closable
|
||||
className="!w-[362px] !p-0"
|
||||
isShow={isShowAvatarPicker}
|
||||
onClose={() => setIsShowAvatarPicker(false)}
|
||||
>
|
||||
<ImageInput onImageInput={handleImageInput} cropShape="round" />
|
||||
<Divider className="m-0" />
|
||||
<Dialog open={isShowAvatarPicker} onOpenChange={open => !open && setIsShowAvatarPicker(false)}>
|
||||
<DialogContent className="!w-[362px] !p-0">
|
||||
<ImageInput onImageInput={handleImageInput} cropShape="round" />
|
||||
<Divider className="m-0" />
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
||||
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
|
||||
{t('iconPicker.cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
||||
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
|
||||
{t('iconPicker.cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
|
||||
{t('iconPicker.ok', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
|
||||
{t('iconPicker.ok', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Modal
|
||||
closable
|
||||
className="!w-[362px] !p-6"
|
||||
isShow={isShowDeleteConfirm}
|
||||
onClose={() => setIsShowDeleteConfirm(false)}
|
||||
>
|
||||
<div className="mb-3 text-text-primary title-2xl-semi-bold">{t('avatar.deleteTitle', { ns: 'common' })}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
|
||||
<Dialog open={isShowDeleteConfirm} onOpenChange={open => !open && setIsShowDeleteConfirm(false)}>
|
||||
<DialogContent className="!w-[362px] !p-6">
|
||||
<div className="mb-3 text-text-primary title-2xl-semi-bold">{t('avatar.deleteTitle', { ns: 'common' })}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
|
||||
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import type { ResponseError } from '@/service/fetch'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import {
|
||||
checkEmailExisted,
|
||||
@ -34,7 +32,6 @@ enum STEP {
|
||||
|
||||
const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const router = useRouter()
|
||||
const [step, setStep] = useState<STEP>(STEP.start)
|
||||
const [code, setCode] = useState<string>('')
|
||||
@ -70,10 +67,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
setStepToken(res.data)
|
||||
}
|
||||
catch (error) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `Error sending verification code: ${error ? (error as any).message : ''}`,
|
||||
})
|
||||
toast.error(`Error sending verification code: ${error ? (error as any).message : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,17 +83,11 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
callback?.(res.token)
|
||||
}
|
||||
else {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: 'Verifying email failed',
|
||||
})
|
||||
toast.error('Verifying email failed')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `Error verifying email: ${error ? (error as any).message : ''}`,
|
||||
})
|
||||
toast.error(`Error verifying email: ${error ? (error as any).message : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,10 +142,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
|
||||
const sendCodeToNewEmail = async () => {
|
||||
if (!isValidEmail(mail)) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: 'Invalid email format',
|
||||
})
|
||||
toast.error('Invalid email format')
|
||||
return
|
||||
}
|
||||
await sendEmail(
|
||||
@ -187,10 +172,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
handleLogout()
|
||||
}
|
||||
catch (error) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `Error changing email: ${error ? (error as any).message : ''}`,
|
||||
})
|
||||
toast.error(`Error changing email: ${error ? (error as any).message : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,187 +181,185 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-warning body-md-medium">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content1"
|
||||
ns="common"
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email }}
|
||||
<Dialog open={show} onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="!w-[420px] !p-6">
|
||||
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-warning body-md-medium">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content1"
|
||||
ns="common"
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3"></div>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={sendCodeToOriginEmail}
|
||||
>
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyOrigin && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content2"
|
||||
ns="common"
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3"></div>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={sendCodeToOriginEmail}
|
||||
>
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyOrigin && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content2"
|
||||
ns="common"
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email }}
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={code.length !== 6}
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={handleVerifyOriginEmail}
|
||||
>
|
||||
{t('account.changeEmail.continue', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToOriginEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.newEmail && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-secondary body-md-regular">{t('account.changeEmail.content3', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
|
||||
value={mail}
|
||||
onChange={e => handleNewEmailValueChange(e.target.value)}
|
||||
destructive={newEmailExited || unAvailableEmail}
|
||||
/>
|
||||
{newEmailExited && (
|
||||
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
{unAvailableEmail && (
|
||||
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={sendCodeToNewEmail}
|
||||
>
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyNew && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content4"
|
||||
ns="common"
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email: mail }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={code.length !== 6}
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={handleVerifyOriginEmail}
|
||||
>
|
||||
{t('account.changeEmail.continue', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToOriginEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.newEmail && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-secondary body-md-regular">{t('account.changeEmail.content3', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
|
||||
value={mail}
|
||||
onChange={e => handleNewEmailValueChange(e.target.value)}
|
||||
destructive={newEmailExited || unAvailableEmail}
|
||||
/>
|
||||
{newEmailExited && (
|
||||
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
{unAvailableEmail && (
|
||||
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={sendCodeToNewEmail}
|
||||
>
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyNew && (
|
||||
<>
|
||||
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="text-text-secondary body-md-regular">
|
||||
<Trans
|
||||
i18nKey="account.changeEmail.content4"
|
||||
ns="common"
|
||||
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
|
||||
values={{ email: mail }}
|
||||
/>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={code.length !== 6}
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={submitNewEmail}
|
||||
>
|
||||
{t('account.changeEmail.changeTo', { ns: 'common', email: mail })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={code.length !== 6}
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={submitNewEmail}
|
||||
>
|
||||
{t('account.changeEmail.changeTo', { ns: 'common', email: mail })}
|
||||
</Button>
|
||||
<Button
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToNewEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToNewEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -7,13 +7,12 @@ import {
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||
import { IS_CE_EDITION, validPassword } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
@ -43,7 +42,6 @@ export default function AccountPage() {
|
||||
const userProfile = userProfileResp?.profile
|
||||
const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: commonQueryKeys.userProfile })
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||
const [editName, setEditName] = useState('')
|
||||
const [editing, setEditing] = useState(false)
|
||||
@ -68,22 +66,19 @@ export default function AccountPage() {
|
||||
try {
|
||||
setEditing(true)
|
||||
await updateUserProfile({ url: 'account/name', body: { name: editName } })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
mutateUserProfile()
|
||||
setEditNameModalVisible(false)
|
||||
setEditing(false)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
toast.error((e as Error).message)
|
||||
setEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const showErrorMessage = (message: string) => {
|
||||
notify({
|
||||
type: 'error',
|
||||
message,
|
||||
})
|
||||
toast.error(message)
|
||||
}
|
||||
const valid = () => {
|
||||
if (!password.trim()) {
|
||||
@ -119,14 +114,14 @@ export default function AccountPage() {
|
||||
repeat_new_password: confirmPassword,
|
||||
},
|
||||
})
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
mutateUserProfile()
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
setEditing(false)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
toast.error((e as Error).message)
|
||||
setEditPasswordModalVisible(false)
|
||||
setEditing(false)
|
||||
}
|
||||
@ -221,119 +216,112 @@ export default function AccountPage() {
|
||||
</div>
|
||||
{
|
||||
editNameModalVisible && (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => setEditNameModalVisible(false)}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="mb-6 text-text-primary title-2xl-semi-bold">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
disabled={editing || !editName}
|
||||
variant="primary"
|
||||
onClick={handleSaveName}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Dialog open={editNameModalVisible} onOpenChange={open => !open && setEditNameModalVisible(false)}>
|
||||
<DialogContent className="!w-[420px] !p-6">
|
||||
<div className="mb-6 text-text-primary title-2xl-semi-bold">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
disabled={editing || !editName}
|
||||
variant="primary"
|
||||
onClick={handleSaveName}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
{
|
||||
editPasswordModalVisible && (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className="mb-6 text-text-primary title-2xl-semi-bold">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
<Dialog open={editPasswordModalVisible} onOpenChange={open => !open && (setEditPasswordModalVisible(false), resetPasswordForm())}>
|
||||
<DialogContent className="!w-[420px] !p-6">
|
||||
<div className="mb-6 text-text-primary title-2xl-semi-bold">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
{showCurrentPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
{showCurrentPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 text-text-secondary system-sm-semibold">
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 text-text-secondary system-sm-semibold">
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
</div>
|
||||
<div className="mt-8 text-text-secondary system-sm-semibold">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={editing}
|
||||
variant="primary"
|
||||
onClick={handleSavePassword}
|
||||
>
|
||||
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 text-text-secondary system-sm-semibold">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={editing}
|
||||
variant="primary"
|
||||
onClick={handleSavePassword}
|
||||
>
|
||||
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
{
|
||||
|
||||
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CustomDialog from '@/app/components/base/dialog'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
@ -28,7 +28,7 @@ export default function FeedBack(props: DeleteAccountProps) {
|
||||
await logout()
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
router.push('/signin')
|
||||
Toast.notify({ type: 'info', message: t('account.deleteSuccessTip', { ns: 'common' }) })
|
||||
toast.info(t('account.deleteSuccessTip', { ns: 'common' }))
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
}, [router, t])
|
||||
|
||||
@ -16,8 +16,8 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
import ContentDialog from '@/app/components/base/content-dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
|
||||
@ -27,10 +27,6 @@ vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ replace: mockReplace }),
|
||||
}))
|
||||
|
||||
vi.mock('use-context-selector', () => ({
|
||||
useContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({ onPlanInfoChanged: mockOnPlanInfoChanged }),
|
||||
}))
|
||||
@ -42,8 +38,16 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
ToastContext: {},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(mockNotify, {
|
||||
success: vi.fn((message, options) => mockNotify({ type: 'success', message, ...options })),
|
||||
error: vi.fn((message, options) => mockNotify({ type: 'error', message, ...options })),
|
||||
warning: vi.fn((message, options) => mockNotify({ type: 'warning', message, ...options })),
|
||||
info: vi.fn((message, options) => mockNotify({ type: 'info', message, ...options })),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
|
||||
@ -3,9 +3,8 @@ import type { CreateAppModalProps } from '@/app/components/explore/create-app-mo
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
@ -24,7 +23,6 @@ type UseAppInfoActionsParams = {
|
||||
|
||||
export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { replace } = useRouter()
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
@ -72,13 +70,13 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
max_active_requests,
|
||||
})
|
||||
closeModal()
|
||||
notify({ type: 'success', message: t('editDone', { ns: 'app' }) })
|
||||
toast(t('editDone', { ns: 'app' }), { type: 'success' })
|
||||
setAppDetail(app)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('editFailed', { ns: 'app' }) })
|
||||
toast(t('editFailed', { ns: 'app' }), { type: 'error' })
|
||||
}
|
||||
}, [appDetail, closeModal, notify, setAppDetail, t])
|
||||
}, [appDetail, closeModal, setAppDetail, t])
|
||||
|
||||
const onCopy: DuplicateAppModalProps['onConfirm'] = useCallback(async ({
|
||||
name,
|
||||
@ -98,15 +96,15 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
mode: appDetail.mode,
|
||||
})
|
||||
closeModal()
|
||||
notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) })
|
||||
toast(t('newApp.appCreated', { ns: 'app' }), { type: 'success' })
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
onPlanInfoChanged()
|
||||
getRedirection(true, newApp, replace)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast(t('newApp.appCreateFailed', { ns: 'app' }), { type: 'error' })
|
||||
}
|
||||
}, [appDetail, closeModal, notify, onPlanInfoChanged, replace, t])
|
||||
}, [appDetail, closeModal, onPlanInfoChanged, replace, t])
|
||||
|
||||
const onExport = useCallback(async (include = false) => {
|
||||
if (!appDetail)
|
||||
@ -117,9 +115,9 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
downloadBlob({ data: file, fileName: `${appDetail.name}.yml` })
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
toast(t('exportFailed', { ns: 'app' }), { type: 'error' })
|
||||
}
|
||||
}, [appDetail, notify, t])
|
||||
}, [appDetail, t])
|
||||
|
||||
const exportCheck = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
@ -145,29 +143,26 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
setSecretEnvList(list)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
toast(t('exportFailed', { ns: 'app' }), { type: 'error' })
|
||||
}
|
||||
}, [appDetail, closeModal, notify, onExport, t])
|
||||
}, [appDetail, closeModal, onExport, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
try {
|
||||
await deleteApp(appDetail.id)
|
||||
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
|
||||
toast(t('appDeleted', { ns: 'app' }), { type: 'success' })
|
||||
invalidateAppList()
|
||||
onPlanInfoChanged()
|
||||
setAppDetail()
|
||||
replace('/apps')
|
||||
}
|
||||
catch (e: unknown) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error && e.message ? `: ${e.message}` : ''}`,
|
||||
})
|
||||
toast(`${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error && e.message ? `: ${e.message}` : ''}`, { type: 'error' })
|
||||
}
|
||||
closeModal()
|
||||
}, [appDetail, closeModal, invalidateAppList, notify, onPlanInfoChanged, replace, setAppDetail, t])
|
||||
}, [appDetail, closeModal, invalidateAppList, onPlanInfoChanged, replace, setAppDetail, t])
|
||||
|
||||
return {
|
||||
appDetail,
|
||||
|
||||
@ -3,6 +3,7 @@ import { RiMoreFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
@ -15,7 +16,6 @@ import { downloadBlob } from '@/utils/download'
|
||||
import ActionButton from '../../base/action-button'
|
||||
import Confirm from '../../base/confirm'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
||||
import Toast from '../../base/toast'
|
||||
import RenameDatasetModal from '../../datasets/rename-modal'
|
||||
import Menu from './menu'
|
||||
|
||||
@ -69,7 +69,7 @@ const DropDown = ({
|
||||
downloadBlob({ data: file, fileName: `${name}.pipeline` })
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
toast(t('exportFailed', { ns: 'app' }), { type: 'error' })
|
||||
}
|
||||
}, [dataset, exportPipelineConfig, handleTrigger, t])
|
||||
|
||||
@ -81,7 +81,7 @@ const DropDown = ({
|
||||
}
|
||||
catch (e: any) {
|
||||
const res = await e.json()
|
||||
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
||||
toast(res?.message || 'Unknown error', { type: 'error' })
|
||||
}
|
||||
finally {
|
||||
handleTrigger()
|
||||
@ -91,7 +91,7 @@ const DropDown = ({
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
|
||||
toast(t('datasetDeleted', { ns: 'dataset' }), { type: 'success' })
|
||||
invalidDatasetList()
|
||||
replace('/datasets')
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ vi.mock('@/context/provider-context', () => ({
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(args => mockToastNotify(args)),
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AnnotationFull from '@/app/components/billing/annotation-full'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import EditItem, { EditItemType } from './edit-item'
|
||||
@ -47,10 +47,7 @@ const AddAnnotationModal: FC<Props> = ({
|
||||
answer,
|
||||
}
|
||||
if (isValid(payload) !== true) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: isValid(payload) as string,
|
||||
})
|
||||
toast.error(isValid(payload) as string)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
import type { Props } from './csv-uploader'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import CSVUploader from './csv-uploader'
|
||||
|
||||
describe('CSVUploader', () => {
|
||||
const notify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => notify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => notify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => notify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => notify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const updateFile = vi.fn()
|
||||
|
||||
const getDropElements = () => {
|
||||
@ -24,9 +36,8 @@ describe('CSVUploader', () => {
|
||||
...props,
|
||||
}
|
||||
return render(
|
||||
<ToastContext.Provider value={{ notify, close: vi.fn() }}>
|
||||
<CSVUploader {...mergedProps} />
|
||||
</ToastContext.Provider>,
|
||||
<CSVUploader {...mergedProps} />,
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,9 @@ import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type Props = {
|
||||
@ -20,7 +19,6 @@ const CSVUploader: FC<Props> = ({
|
||||
updateFile,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
@ -50,7 +48,7 @@ const CSVUploader: FC<Props> = ({
|
||||
return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
toast.error(t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }))
|
||||
return
|
||||
}
|
||||
updateFile(files[0])
|
||||
|
||||
@ -2,17 +2,10 @@ import type { Mock } from 'vitest'
|
||||
import type { IBatchModalProps } from './index'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation'
|
||||
import BatchModal, { ProcessStatus } from './index'
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/annotation', () => ({
|
||||
annotationBatchImport: vi.fn(),
|
||||
checkAnnotationBatchImportProgress: vi.fn(),
|
||||
@ -49,7 +42,7 @@ vi.mock('@/app/components/billing/annotation-full', () => ({
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
const mockNotify = Toast.notify as Mock
|
||||
const mockNotify = vi.fn()
|
||||
const useProviderContextMock = useProviderContext as Mock
|
||||
const annotationBatchImportMock = annotationBatchImport as Mock
|
||||
const checkAnnotationBatchImportProgressMock = checkAnnotationBatchImportProgress as Mock
|
||||
|
||||
@ -7,7 +7,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AnnotationFull from '@/app/components/billing/annotation-full'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation'
|
||||
@ -46,7 +46,6 @@ const BatchModal: FC<IBatchModalProps> = ({
|
||||
}, [isShow])
|
||||
|
||||
const [importStatus, setImportStatus] = useState<ProcessStatus | string>()
|
||||
const notify = Toast.notify
|
||||
const checkProcess = async (jobID: string) => {
|
||||
try {
|
||||
const res = await checkAnnotationBatchImportProgress({ jobID, appId })
|
||||
@ -54,15 +53,15 @@ const BatchModal: FC<IBatchModalProps> = ({
|
||||
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
|
||||
setTimeout(() => checkProcess(res.job_id), 2500)
|
||||
if (res.job_status === ProcessStatus.ERROR)
|
||||
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}` })
|
||||
toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}`)
|
||||
if (res.job_status === ProcessStatus.COMPLETED) {
|
||||
notify({ type: 'success', message: `${t('batchModal.completed', { ns: 'appAnnotation' })}` })
|
||||
toast.success(`${t('batchModal.completed', { ns: 'appAnnotation' })}`)
|
||||
onAdded()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +77,7 @@ const BatchModal: FC<IBatchModalProps> = ({
|
||||
checkProcess(res.job_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { IToastProps, ToastHandle } from '@/app/components/base/toast'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import EditAnnotationModal from './index'
|
||||
|
||||
const { mockAddAnnotation, mockEditAnnotation } = vi.hoisted(() => ({
|
||||
@ -37,10 +36,8 @@ vi.mock('@/app/components/billing/annotation-full', () => ({
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
type ToastNotifyProps = Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className' | 'customComponent' | 'onClose'>
|
||||
type ToastWithNotify = typeof Toast & { notify: (props: ToastNotifyProps) => ToastHandle }
|
||||
const toastWithNotify = Toast as unknown as ToastWithNotify
|
||||
const toastNotifySpy = vi.spyOn(toastWithNotify, 'notify').mockReturnValue({ clear: vi.fn() })
|
||||
const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
|
||||
const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
|
||||
describe('EditAnnotationModal', () => {
|
||||
const defaultProps = {
|
||||
@ -55,7 +52,8 @@ describe('EditAnnotationModal', () => {
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
toastNotifySpy.mockRestore()
|
||||
toastSuccessSpy.mockRestore()
|
||||
toastErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@ -437,10 +435,7 @@ describe('EditAnnotationModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'API Error',
|
||||
type: 'error',
|
||||
})
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('API Error')
|
||||
})
|
||||
expect(mockOnAdded).not.toHaveBeenCalled()
|
||||
|
||||
@ -475,10 +470,7 @@ describe('EditAnnotationModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'common.api.actionFailed',
|
||||
type: 'error',
|
||||
})
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('common.api.actionFailed')
|
||||
})
|
||||
expect(mockOnAdded).not.toHaveBeenCalled()
|
||||
|
||||
@ -517,10 +509,7 @@ describe('EditAnnotationModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'API Error',
|
||||
type: 'error',
|
||||
})
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('API Error')
|
||||
})
|
||||
expect(mockOnEdited).not.toHaveBeenCalled()
|
||||
|
||||
@ -557,10 +546,7 @@ describe('EditAnnotationModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'common.api.actionFailed',
|
||||
type: 'error',
|
||||
})
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('common.api.actionFailed')
|
||||
})
|
||||
expect(mockOnEdited).not.toHaveBeenCalled()
|
||||
|
||||
@ -641,10 +627,7 @@ describe('EditAnnotationModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'common.api.actionSuccess',
|
||||
type: 'success',
|
||||
})
|
||||
expect(toastSuccessSpy).toHaveBeenCalledWith('common.api.actionSuccess')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AnnotationFull from '@/app/components/billing/annotation-full'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
@ -72,18 +72,12 @@ const EditAnnotationModal: FC<Props> = ({
|
||||
onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer)
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
message: t('api.actionSuccess', { ns: 'common' }) as string,
|
||||
type: 'success',
|
||||
})
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }) as string)
|
||||
}
|
||||
catch (error) {
|
||||
const fallbackMessage = t('api.actionFailed', { ns: 'common' }) as string
|
||||
const message = error instanceof Error && error.message ? error.message : fallbackMessage
|
||||
Toast.notify({
|
||||
message,
|
||||
type: 'error',
|
||||
})
|
||||
toast.error(message)
|
||||
// Re-throw to preserve edit mode behavior for UI components
|
||||
throw error
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import type { AnnotationItem } from './type'
|
||||
import type { App } from '@/types/app'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
addAnnotation,
|
||||
@ -17,10 +17,6 @@ import { AppModeEnum } from '@/types/app'
|
||||
import Annotation from './index'
|
||||
import { JobStatus } from './type'
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounce: (value: any) => value,
|
||||
}))
|
||||
@ -95,7 +91,23 @@ vi.mock('./view-annotation-modal', () => ({
|
||||
vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ default: (props: any) => props.isShow ? <div data-testid="config-modal" /> : null }))
|
||||
vi.mock('@/app/components/billing/annotation-full/modal', () => ({ default: (props: any) => props.show ? <div data-testid="annotation-full-modal" /> : null }))
|
||||
|
||||
const mockNotify = Toast.notify as Mock
|
||||
const mockNotify = vi.fn()
|
||||
vi.spyOn(toast, 'success').mockImplementation((message, options) => {
|
||||
mockNotify({ type: 'success', message, ...options })
|
||||
return 'toast-success-id'
|
||||
})
|
||||
vi.spyOn(toast, 'error').mockImplementation((message, options) => {
|
||||
mockNotify({ type: 'error', message, ...options })
|
||||
return 'toast-error-id'
|
||||
})
|
||||
vi.spyOn(toast, 'warning').mockImplementation((message, options) => {
|
||||
mockNotify({ type: 'warning', message, ...options })
|
||||
return 'toast-warning-id'
|
||||
})
|
||||
vi.spyOn(toast, 'info').mockImplementation((message, options) => {
|
||||
mockNotify({ type: 'info', message, ...options })
|
||||
return 'toast-info-id'
|
||||
})
|
||||
const addAnnotationMock = addAnnotation as Mock
|
||||
const delAnnotationMock = delAnnotation as Mock
|
||||
const delAnnotationsMock = delAnnotations as Mock
|
||||
|
||||
@ -15,6 +15,7 @@ import { MessageFast } from '@/app/components/base/icons/src/vender/solid/commun
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -22,7 +23,6 @@ import { addAnnotation, delAnnotation, delAnnotations, fetchAnnotationConfig as
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { sleep } from '@/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Toast from '../../base/toast'
|
||||
import EmptyElement from './empty-element'
|
||||
import Filter from './filter'
|
||||
import HeaderOpts from './header-opts'
|
||||
@ -98,14 +98,14 @@ const Annotation: FC<Props> = (props) => {
|
||||
|
||||
const handleAdd = async (payload: AnnotationItemBasic) => {
|
||||
await addAnnotation(appDetail.id, payload)
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
||||
const handleRemove = async (id: string) => {
|
||||
await delAnnotation(appDetail.id, id)
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
@ -113,13 +113,13 @@ const Annotation: FC<Props> = (props) => {
|
||||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
await delAnnotations(appDetail.id, selectedIds)
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
setSelectedIds([])
|
||||
}
|
||||
catch (e: any) {
|
||||
Toast.notify({ type: 'error', message: e.message || t('api.actionFailed', { ns: 'common' }) })
|
||||
toast.error(e.message || t('api.actionFailed', { ns: 'common' }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
if (!currItem)
|
||||
return
|
||||
await editAnnotation(appDetail.id, currItem.id, { question, answer })
|
||||
Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' })
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
@ -170,10 +170,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
const { job_id: jobId }: any = await updateAnnotationStatus(appDetail.id, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold)
|
||||
await ensureJobCompleted(jobId, AnnotationEnableStatus.disable)
|
||||
await fetchAnnotationConfig()
|
||||
Toast.notify({
|
||||
message: t('api.actionSuccess', { ns: 'common' }),
|
||||
type: 'success',
|
||||
})
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -263,10 +260,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
await updateAnnotationScore(appDetail.id, annotationId, score)
|
||||
|
||||
await fetchAnnotationConfig()
|
||||
Toast.notify({
|
||||
message: t('api.actionSuccess', { ns: 'common' }),
|
||||
type: 'success',
|
||||
})
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
annotationConfig={annotationConfig!}
|
||||
|
||||
@ -2,9 +2,9 @@ import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models
|
||||
import type { App } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||
import Toast from '../../base/toast'
|
||||
import AccessControlDialog from './access-control-dialog'
|
||||
import AccessControlItem from './access-control-item'
|
||||
import AddMemberOrGroupDialog from './add-member-or-group-pop'
|
||||
@ -303,7 +303,7 @@ describe('AccessControl', () => {
|
||||
it('should initialize menu from app and call update on confirm', async () => {
|
||||
const onClose = vi.fn()
|
||||
const onConfirm = vi.fn()
|
||||
const toastSpy = vi.spyOn(Toast, 'notify').mockReturnValue({})
|
||||
const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
|
||||
useAccessControlStore.setState({
|
||||
specificGroups: [baseGroup],
|
||||
specificMembers: [baseMember],
|
||||
@ -336,7 +336,7 @@ describe('AccessControl', () => {
|
||||
{ subjectId: baseMember.id, subjectType: SubjectType.ACCOUNT },
|
||||
],
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('app.accessControlDialog.updateSuccess')
|
||||
expect(onConfirm).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,12 +5,12 @@ import { Description as DialogDescription, DialogTitle } from '@headlessui/react
|
||||
import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||
import { useUpdateAccessMode } from '@/service/access-control'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import Button from '../../base/button'
|
||||
import Toast from '../../base/toast'
|
||||
import AccessControlDialog from './access-control-dialog'
|
||||
import AccessControlItem from './access-control-item'
|
||||
import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members'
|
||||
@ -61,7 +61,7 @@ export default function AccessControl(props: AccessControlProps) {
|
||||
submitData.subjects = subjects
|
||||
}
|
||||
await updateAccessMode(submitData)
|
||||
Toast.notify({ type: 'success', message: t('accessControlDialog.updateSuccess', { ns: 'app' }) })
|
||||
toast.success(t('accessControlDialog.updateSuccess', { ns: 'app' }))
|
||||
onConfirm?.()
|
||||
}, [updateAccessMode, app, specificGroups, specificMembers, t, onConfirm, currentMenu])
|
||||
return (
|
||||
|
||||
@ -28,6 +28,7 @@ import {
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
@ -47,7 +48,6 @@ import { AppModeEnum } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Divider from '../../base/divider'
|
||||
import Loading from '../../base/loading'
|
||||
import Toast from '../../base/toast'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
|
||||
@ -264,7 +264,7 @@ const AppPublisher = ({
|
||||
throw new Error('No app found in Explore')
|
||||
}, {
|
||||
onError: (err) => {
|
||||
Toast.notify({ type: 'error', message: `${err.message || err}` })
|
||||
toast.error(`${err.message || err}`)
|
||||
},
|
||||
})
|
||||
}, [appDetail?.id, openAsyncWindow])
|
||||
@ -290,7 +290,7 @@ const AppPublisher = ({
|
||||
window.open(result.redirect_url, '_blank')
|
||||
}
|
||||
catch (error: any) {
|
||||
Toast.notify({ type: 'error', message: error.message || t('common.publishToMarketplaceFailed', { ns: 'workflow' }) })
|
||||
toast.error(error.message || t('common.publishToMarketplaceFailed', { ns: 'workflow' }))
|
||||
}
|
||||
finally {
|
||||
setPublishingToMarketplace(false)
|
||||
|
||||
@ -5,7 +5,7 @@ import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import Button from '../../base/button'
|
||||
import Input from '../../base/input'
|
||||
import Textarea from '../../base/textarea'
|
||||
@ -35,10 +35,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
const handlePublish = () => {
|
||||
if (title.length > TITLE_MAX_LENGTH) {
|
||||
setTitleError(true)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('versionHistory.editField.titleLengthLimit', { ns: 'workflow', limit: TITLE_MAX_LENGTH }),
|
||||
})
|
||||
toast.error(t('versionHistory.editField.titleLengthLimit', { ns: 'workflow', limit: TITLE_MAX_LENGTH }))
|
||||
return
|
||||
}
|
||||
else {
|
||||
@ -48,10 +45,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
|
||||
if (releaseNotes.length > RELEASE_NOTES_MAX_LENGTH) {
|
||||
setReleaseNotesError(true)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('versionHistory.editField.releaseNotesLengthLimit', { ns: 'workflow', limit: RELEASE_NOTES_MAX_LENGTH }),
|
||||
})
|
||||
toast.error(t('versionHistory.editField.releaseNotesLengthLimit', { ns: 'workflow', limit: RELEASE_NOTES_MAX_LENGTH }))
|
||||
return
|
||||
}
|
||||
else {
|
||||
|
||||
@ -20,8 +20,12 @@ import {
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/app/components/base/ui/tooltip'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
@ -74,7 +78,6 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
showSelectDataSet,
|
||||
externalDataToolsConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const { notify } = useToastContext()
|
||||
const { setShowExternalDataToolModal } = useModalContext()
|
||||
const handleOpenExternalDataToolModal = () => {
|
||||
setShowExternalDataToolModal({
|
||||
@ -94,7 +97,7 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable) {
|
||||
notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -180,13 +183,18 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
<div className="text-sm font-semibold uppercase text-indigo-800">
|
||||
{t('pageTitle.line1', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<span className="i-ri-question-line ml-1 h-4 w-4 shrink-0 text-text-quaternary" />
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<div className="w-[180px]">
|
||||
{t('promptTip', { ns: 'appDebug' })}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(s.optionWrap, 'items-center space-x-1')}>
|
||||
|
||||
@ -17,8 +17,12 @@ import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/app/components/base/ui/tooltip'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
@ -72,7 +76,6 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
showSelectDataSet,
|
||||
externalDataToolsConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const { notify } = useToastContext()
|
||||
const { setShowExternalDataToolModal } = useModalContext()
|
||||
const handleOpenExternalDataToolModal = () => {
|
||||
setShowExternalDataToolModal({
|
||||
@ -92,7 +95,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable) {
|
||||
notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -180,13 +183,18 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="h2 text-text-secondary system-sm-semibold-uppercase">{mode !== AppModeEnum.COMPLETION ? t('chatSubTitle', { ns: 'appDebug' }) : t('completionSubTitle', { ns: 'appDebug' })}</div>
|
||||
{!readonly && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<span className="i-ri-question-line ml-1 h-4 w-4 shrink-0 text-text-quaternary" />
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<div className="w-[180px]">
|
||||
{t('promptTip', { ns: 'appDebug' })}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
@ -15,7 +15,7 @@ import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
|
||||
@ -98,10 +98,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const checkVariableName = useCallback((value: string, canBeEmpty?: boolean) => {
|
||||
const { isValid, errorMessageKey } = checkKeys([value], canBeEmpty)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }),
|
||||
})
|
||||
toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -221,10 +218,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const value = e.target.value
|
||||
const { isValid, errorKey, errorMessageKey } = checkKeys([value], true)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }),
|
||||
})
|
||||
toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }))
|
||||
return
|
||||
}
|
||||
handlePayloadChange('variable')(e.target.value)
|
||||
@ -266,7 +260,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
return
|
||||
|
||||
if (!tempPayload.label) {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' }) })
|
||||
toast.error(t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
if (isStringInput || type === InputVarType.number) {
|
||||
@ -274,7 +268,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
}
|
||||
else if (type === InputVarType.select) {
|
||||
if (options?.length === 0) {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' }) })
|
||||
toast.error(t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
const obj: Record<string, boolean> = {}
|
||||
@ -287,7 +281,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
obj[o] = true
|
||||
})
|
||||
if (hasRepeatedItem) {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) })
|
||||
toast.error(t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
@ -297,12 +291,12 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
)) {
|
||||
if (tempPayload.allowed_file_types?.length === 0) {
|
||||
const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.supportFileTypes', { ns: 'appDebug' }) })
|
||||
Toast.notify({ type: 'error', message: errorMessages })
|
||||
toast.error(errorMessages)
|
||||
return
|
||||
}
|
||||
if (tempPayload.allowed_file_types?.includes(SupportUploadFileTypes.custom) && !tempPayload.allowed_file_extensions?.length) {
|
||||
const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.custom.name', { ns: 'appDebug' }) })
|
||||
Toast.notify({ type: 'error', message: errorMessages })
|
||||
toast.error(errorMessages)
|
||||
return
|
||||
}
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
@ -312,12 +306,12 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
try {
|
||||
const schema = JSON.parse(normalizedJsonSchema)
|
||||
if (schema?.type !== 'object') {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) })
|
||||
toast.error(t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) })
|
||||
toast.error(t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,13 +5,13 @@ import type { PromptVariable } from '@/models/debug'
|
||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import DebugConfigurationContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index'
|
||||
|
||||
const notifySpy = vi.spyOn(Toast, 'notify').mockImplementation(vi.fn())
|
||||
const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
|
||||
const setShowExternalDataToolModal = vi.fn()
|
||||
|
||||
@ -112,7 +112,7 @@ describe('ConfigVar', () => {
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
toastErrorSpy.mockClear()
|
||||
})
|
||||
|
||||
it('should show empty state when no variables exist', () => {
|
||||
@ -152,7 +152,7 @@ describe('ConfigVar', () => {
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
toastErrorSpy.mockClear()
|
||||
})
|
||||
|
||||
it('should add a text variable when selecting the string option', async () => {
|
||||
@ -218,7 +218,7 @@ describe('ConfigVar', () => {
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
toastErrorSpy.mockClear()
|
||||
})
|
||||
|
||||
it('should save updates when editing a basic variable', async () => {
|
||||
@ -268,7 +268,7 @@ describe('ConfigVar', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(Toast.notify).toHaveBeenCalled()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(onPromptVariablesChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -294,7 +294,7 @@ describe('ConfigVar', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(Toast.notify).toHaveBeenCalled()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(onPromptVariablesChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -306,7 +306,7 @@ describe('ConfigVar', () => {
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
toastErrorSpy.mockClear()
|
||||
})
|
||||
|
||||
it('should remove variable directly when context confirmation is not required', () => {
|
||||
@ -359,7 +359,7 @@ describe('ConfigVar', () => {
|
||||
latestSortableProps = null
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
toastErrorSpy.mockClear()
|
||||
})
|
||||
|
||||
it('should append external data tool variables from event emitter', () => {
|
||||
|
||||
@ -12,8 +12,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
@ -108,10 +108,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
})
|
||||
const duplicateError = getDuplicateError(newPromptVariables)
|
||||
if (duplicateError) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(duplicateError.errorMsgKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug', key: t(duplicateError.typeName as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' }) }) as string,
|
||||
})
|
||||
toast.error(t(duplicateError.errorMsgKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug', key: t(duplicateError.typeName as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' }) }) as string)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -161,7 +158,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable && i !== index) {
|
||||
Toast.notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
CopyCheck,
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -32,8 +32,6 @@ const Editor: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const {
|
||||
modelConfig,
|
||||
@ -59,14 +57,14 @@ const Editor: FC<Props> = ({
|
||||
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable) {
|
||||
notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < externalDataToolsConfig.length; i++) {
|
||||
if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) {
|
||||
notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,9 +23,9 @@ import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
@ -161,13 +161,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
|
||||
const isValid = () => {
|
||||
if (instruction.trim() === '') {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('errorMsg.fieldRequired', {
|
||||
ns: 'common',
|
||||
field: t('generate.instruction', { ns: 'appDebug' }),
|
||||
}),
|
||||
})
|
||||
toast.error(t('errorMsg.fieldRequired', {
|
||||
ns: 'common',
|
||||
field: t('generate.instruction', { ns: 'appDebug' }),
|
||||
}))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -242,10 +239,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
} as GenRes
|
||||
if (error) {
|
||||
hasError = true
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
toast.error(error)
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -260,10 +254,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
apiRes = res
|
||||
if (error) {
|
||||
hasError = true
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
toast.error(error)
|
||||
}
|
||||
}
|
||||
if (!hasError)
|
||||
|
||||
@ -6,7 +6,7 @@ import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
|
||||
import PromptRes from './prompt-res'
|
||||
import PromptResInWorkflow from './prompt-res-in-workflow'
|
||||
@ -54,7 +54,7 @@ const Result: FC<Props> = ({
|
||||
className="px-2"
|
||||
onClick={() => {
|
||||
copy(current.modified)
|
||||
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
>
|
||||
<RiClipboardLine className="h-4 w-4 text-text-secondary" />
|
||||
|
||||
@ -15,7 +15,7 @@ import Confirm from '@/app/components/base/confirm'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
@ -90,13 +90,10 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
|
||||
const isValid = () => {
|
||||
if (instruction.trim() === '') {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('errorMsg.fieldRequired', {
|
||||
ns: 'common',
|
||||
field: t('code.instruction', { ns: 'appDebug' }),
|
||||
}),
|
||||
})
|
||||
toast.error(t('errorMsg.fieldRequired', {
|
||||
ns: 'common',
|
||||
field: t('code.instruction', { ns: 'appDebug' }),
|
||||
}))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -149,10 +146,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
res.modified = (res as any).code
|
||||
|
||||
if (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
toast.error(error)
|
||||
}
|
||||
else {
|
||||
addVersion(res)
|
||||
|
||||
@ -5,7 +5,7 @@ import type { DatasetConfigs } from '@/models/debug'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
useCurrentProviderAndModel,
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel,
|
||||
@ -46,7 +46,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
||||
const mockedUseModelListAndDefaultModelAndCurrentProviderAndModel = useModelListAndDefaultModelAndCurrentProviderAndModel as MockedFunction<typeof useModelListAndDefaultModelAndCurrentProviderAndModel>
|
||||
const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as MockedFunction<typeof useCurrentProviderAndModel>
|
||||
|
||||
let toastNotifySpy: MockInstance
|
||||
let toastErrorSpy: MockInstance
|
||||
|
||||
const baseRetrievalConfig: RetrievalConfig = {
|
||||
search_method: RETRIEVE_METHOD.semantic,
|
||||
@ -172,7 +172,7 @@ const createDatasetConfigs = (overrides: Partial<DatasetConfigs> = {}): DatasetC
|
||||
describe('ConfigContent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({}))
|
||||
toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
mockedUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({
|
||||
modelList: [],
|
||||
defaultModel: undefined,
|
||||
@ -186,7 +186,7 @@ describe('ConfigContent', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
toastNotifySpy.mockRestore()
|
||||
toastErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
// State management
|
||||
@ -331,10 +331,7 @@ describe('ConfigContent', () => {
|
||||
await user.click(screen.getByText('common.modelProvider.rerankModel.key'))
|
||||
|
||||
// Assert
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'workflow.errorMsg.rerankModelRequired',
|
||||
})
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired')
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reranking_mode: RerankingModeEnum.RerankingModel,
|
||||
@ -373,10 +370,7 @@ describe('ConfigContent', () => {
|
||||
await user.click(screen.getByRole('switch'))
|
||||
|
||||
// Assert
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'workflow.errorMsg.rerankModelRequired',
|
||||
})
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired')
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reranking_enable: true,
|
||||
|
||||
@ -15,8 +15,8 @@ import Divider from '@/app/components/base/divider'
|
||||
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
|
||||
import TopKItem from '@/app/components/base/param-item/top-k-item'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
@ -136,7 +136,7 @@ const ConfigContent: FC<Props> = ({
|
||||
return
|
||||
|
||||
if (mode === RerankingModeEnum.RerankingModel && !currentRerankModel)
|
||||
Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) })
|
||||
toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' }))
|
||||
|
||||
onChange({
|
||||
...datasetConfigs,
|
||||
@ -179,7 +179,7 @@ const ConfigContent: FC<Props> = ({
|
||||
|
||||
const handleManuallyToggleRerank = useCallback((enable: boolean) => {
|
||||
if (!currentRerankModel && enable)
|
||||
Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) })
|
||||
toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' }))
|
||||
onChange({
|
||||
...datasetConfigs,
|
||||
reranking_enable: enable,
|
||||
|
||||
@ -3,7 +3,6 @@ import type { DataSet } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -14,6 +13,19 @@ import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import SettingsModal from './index'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
@ -183,13 +195,12 @@ const createDataset = (overrides: Partial<DataSet> = {}, retrievalOverrides: Par
|
||||
|
||||
const renderWithProviders = (dataset: DataSet) => {
|
||||
return render(
|
||||
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
|
||||
<SettingsModal
|
||||
currentDataset={dataset}
|
||||
onCancel={mockOnCancel}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
</ToastContext.Provider>,
|
||||
<SettingsModal
|
||||
currentDataset={dataset}
|
||||
onCancel={mockOnCancel}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import IndexMethod from '@/app/components/datasets/settings/index-method'
|
||||
@ -51,7 +51,6 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const ref = useRef(null)
|
||||
const isExternal = currentDataset.provider === 'external'
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
@ -96,7 +95,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
if (loading)
|
||||
return
|
||||
if (!localeCurrentDataset.name?.trim()) {
|
||||
notify({ type: 'error', message: t('form.nameError', { ns: 'datasetSettings' }) })
|
||||
toast.error(t('form.nameError', { ns: 'datasetSettings' }))
|
||||
return
|
||||
}
|
||||
if (
|
||||
@ -106,7 +105,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
indexMethod,
|
||||
})
|
||||
) {
|
||||
notify({ type: 'error', message: t('datasetConfig.rerankModelRequired', { ns: 'appDebug' }) })
|
||||
toast.error(t('datasetConfig.rerankModelRequired', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
try {
|
||||
@ -146,7 +145,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
})
|
||||
}
|
||||
await updateDatasetSetting(requestParams)
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
onSave({
|
||||
...localeCurrentDataset,
|
||||
indexing_technique: indexMethod,
|
||||
@ -154,7 +153,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
})
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
|
||||
@ -386,13 +386,6 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock toast context
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: vi.fn(() => ({
|
||||
notify: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock hooks/use-timestamp
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
default: vi.fn(() => ({
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app'
|
||||
@ -377,6 +376,19 @@ const renderDebug = (options: {
|
||||
} = {}) => {
|
||||
const onSetting = vi.fn()
|
||||
const notify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => notify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => notify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => notify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => notify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const props: ComponentProps<typeof Debug> = {
|
||||
isAPIKeySet: true,
|
||||
onSetting,
|
||||
@ -392,11 +404,10 @@ const renderDebug = (options: {
|
||||
}
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify, close: vi.fn() }}>
|
||||
<ConfigContext.Provider value={createContextValue(options.contextValue)}>
|
||||
<Debug {...props} />
|
||||
</ConfigContext.Provider>
|
||||
</ToastContext.Provider>,
|
||||
<ConfigContext.Provider value={createContextValue(options.contextValue)}>
|
||||
<Debug {...props} />
|
||||
</ConfigContext.Provider>,
|
||||
|
||||
)
|
||||
|
||||
return { onSetting, notify, props }
|
||||
|
||||
@ -29,8 +29,12 @@ import Button from '@/app/components/base/button'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import PromptLogModal from '@/app/components/base/prompt-log-modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/app/components/base/ui/tooltip'
|
||||
import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
@ -139,22 +143,20 @@ const Debug: FC<IDebug> = ({
|
||||
setIsShowFormattingChangeConfirm(false)
|
||||
setFormattingChanged(false)
|
||||
}
|
||||
|
||||
const { notify } = useContext(ToastContext)
|
||||
const logError = useCallback((message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}, [notify])
|
||||
toast.error(message)
|
||||
}, [])
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
|
||||
const checkCanSend = useCallback(() => {
|
||||
if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) {
|
||||
if (modelModeType === ModelModeType.completion) {
|
||||
if (!hasSetBlockStatus.history) {
|
||||
notify({ type: 'error', message: t('otherError.historyNoBeEmpty', { ns: 'appDebug' }) })
|
||||
toast.error(t('otherError.historyNoBeEmpty', { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
if (!hasSetBlockStatus.query) {
|
||||
notify({ type: 'error', message: t('otherError.queryNoBeEmpty', { ns: 'appDebug' }) })
|
||||
toast.error(t('otherError.queryNoBeEmpty', { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -180,7 +182,7 @@ const Debug: FC<IDebug> = ({
|
||||
}
|
||||
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
@ -194,7 +196,6 @@ const Debug: FC<IDebug> = ({
|
||||
modelConfig.configs.prompt_variables,
|
||||
t,
|
||||
logError,
|
||||
notify,
|
||||
modelModeType,
|
||||
])
|
||||
|
||||
@ -205,7 +206,7 @@ const Debug: FC<IDebug> = ({
|
||||
|
||||
const sendTextCompletion = async () => {
|
||||
if (isResponding) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -420,27 +421,24 @@ const Debug: FC<IDebug> = ({
|
||||
<>
|
||||
{
|
||||
!readonly && (
|
||||
<TooltipPlus
|
||||
popupContent={t('operation.refresh', { ns: 'common' })}
|
||||
>
|
||||
<ActionButton onClick={clearConversation}>
|
||||
<RefreshCcw01 className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
|
||||
</TooltipPlus>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<ActionButton onClick={clearConversation}><RefreshCcw01 className="h-4 w-4" /></ActionButton>} />
|
||||
<TooltipContent>
|
||||
{t('operation.refresh', { ns: 'common' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
varList.length > 0 && (
|
||||
<div className="relative ml-1 mr-2">
|
||||
<TooltipPlus
|
||||
popupContent={t('panel.userInputField', { ns: 'workflow' })}
|
||||
>
|
||||
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}>
|
||||
<RiEqualizer2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</TooltipPlus>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}><RiEqualizer2Line className="h-4 w-4" /></ActionButton>} />
|
||||
<TooltipContent>
|
||||
{t('panel.userInputField', { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{expanded && <div className="absolute bottom-[-14px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg" />}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -27,7 +27,6 @@ import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
|
||||
import Config from '@/app/components/app/configuration/config'
|
||||
@ -49,8 +48,7 @@ import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
@ -94,7 +92,6 @@ type PublishConfig = {
|
||||
|
||||
const Configuration: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext()
|
||||
|
||||
const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
|
||||
@ -493,11 +490,11 @@ const Configuration: FC = () => {
|
||||
isAdvancedMode,
|
||||
)
|
||||
if (Object.keys(removedDetails).length)
|
||||
Toast.notify({ type: 'warning', message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` })
|
||||
toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}`)
|
||||
setCompletionParams(filtered)
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) })
|
||||
toast.error(t('error', { ns: 'common' }))
|
||||
setCompletionParams({})
|
||||
}
|
||||
}
|
||||
@ -769,23 +766,23 @@ const Configuration: FC = () => {
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
|
||||
if (promptEmpty) {
|
||||
notify({ type: 'error', message: t('otherError.promptNoBeEmpty', { ns: 'appDebug' }) })
|
||||
toast.error(t('otherError.promptNoBeEmpty', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) {
|
||||
if (modelModeType === ModelModeType.completion) {
|
||||
if (!hasSetBlockStatus.history) {
|
||||
notify({ type: 'error', message: t('otherError.historyNoBeEmpty', { ns: 'appDebug' }) })
|
||||
toast.error(t('otherError.historyNoBeEmpty', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
if (!hasSetBlockStatus.query) {
|
||||
notify({ type: 'error', message: t('otherError.queryNoBeEmpty', { ns: 'appDebug' }) })
|
||||
toast.error(t('otherError.queryNoBeEmpty', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contextVarEmpty) {
|
||||
notify({ type: 'error', message: t('feature.dataSet.queryVariable.contextVarNotEmpty', { ns: 'appDebug' }) })
|
||||
toast.error(t('feature.dataSet.queryVariable.contextVarNotEmpty', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
@ -851,7 +848,7 @@ const Configuration: FC = () => {
|
||||
modelConfig: newModelConfig,
|
||||
completionParams,
|
||||
})
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
toast.success(t('api.success', { ns: 'common' }))
|
||||
|
||||
setCanReturnToSimpleMode(false)
|
||||
return true
|
||||
|
||||
@ -11,9 +11,9 @@ import Button from '@/app/components/base/button'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
@ -39,7 +39,6 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const locale = useLocale()
|
||||
const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
@ -133,37 +132,34 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
|
||||
const handleSave = () => {
|
||||
if (!localeData.type) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!localeData.label) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!localeData.variable) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.variable && !/^[a-z_]\w{0,29}$/i.test(localeData.variable)) {
|
||||
notify({ type: 'error', message: t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }) })
|
||||
toast.error(t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
|
||||
for (let i = 0; i < currentProvider.form_schema.length; i++) {
|
||||
if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
|
||||
})
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -180,122 +176,128 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
const action = data.type ? t('operation.edit', { ns: 'common' }) : t('operation.add', { ns: 'common' })
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={noop}
|
||||
className="!w-[640px] !max-w-none !p-8 !pb-6"
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={noop}
|
||||
>
|
||||
<div className="mb-2 text-xl font-semibold text-text-primary">
|
||||
{`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`}
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm font-medium leading-9 text-text-primary">
|
||||
{t('apiBasedExtension.type', { ns: 'common' })}
|
||||
<DialogContent className="!w-[640px] !max-w-none !p-8 !pb-6">
|
||||
<div className="mb-2 text-xl font-semibold text-text-primary">
|
||||
{`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`}
|
||||
</div>
|
||||
<SimpleSelect
|
||||
defaultValue={localeData.type}
|
||||
items={providers.map((option) => {
|
||||
return {
|
||||
value: option.key,
|
||||
name: option.name,
|
||||
}
|
||||
})}
|
||||
onSelect={item => handleDataTypeChange(item.value as string)}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm font-medium leading-9 text-text-primary">
|
||||
{t('feature.tools.modal.name.title', { ns: 'appDebug' })}
|
||||
<div className="py-2">
|
||||
<div className="text-sm font-medium leading-9 text-text-primary">
|
||||
{t('apiBasedExtension.type', { ns: 'common' })}
|
||||
</div>
|
||||
<Select
|
||||
defaultValue={localeData.type}
|
||||
onValueChange={value => value && handleDataTypeChange(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label={t('apiBasedExtension.type', { ns: 'common' })}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[354px]">
|
||||
{providers.map(option => (
|
||||
<SelectItem key={option.key} value={option.key}>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
value={localeData.label || ''}
|
||||
onChange={e => handleValueChange({ label: e.target.value })}
|
||||
className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none"
|
||||
placeholder={t('feature.tools.modal.name.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
<AppIcon
|
||||
size="large"
|
||||
onClick={() => { setShowEmojiPicker(true) }}
|
||||
className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border"
|
||||
icon={localeData.icon}
|
||||
background={localeData.icon_background}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm font-medium leading-9 text-text-primary">
|
||||
{t('feature.tools.modal.variableName.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<input
|
||||
value={localeData.variable || ''}
|
||||
onChange={e => handleValueChange({ variable: e.target.value })}
|
||||
className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none"
|
||||
placeholder={t('feature.tools.modal.variableName.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
localeData.type === 'api' && (
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
|
||||
{t('apiBasedExtension.selector.title', { ns: 'common' })}
|
||||
<a
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs font-normal text-text-tertiary hover:text-text-accent"
|
||||
>
|
||||
<BookOpen01 className="mr-1 h-3 w-3 text-text-tertiary group-hover:text-text-accent" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
</a>
|
||||
</div>
|
||||
<ApiBasedExtensionSelector
|
||||
value={localeData.config?.api_based_extension_id || ''}
|
||||
onChange={handleDataApiBasedChange}
|
||||
<div className="py-2">
|
||||
<div className="text-sm font-medium leading-9 text-text-primary">
|
||||
{t('feature.tools.modal.name.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
value={localeData.label || ''}
|
||||
onChange={e => handleValueChange({ label: e.target.value })}
|
||||
className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none"
|
||||
placeholder={t('feature.tools.modal.name.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
<AppIcon
|
||||
size="large"
|
||||
onClick={() => { setShowEmojiPicker(true) }}
|
||||
className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border"
|
||||
icon={localeData.icon}
|
||||
background={localeData.icon_background}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm font-medium leading-9 text-text-primary">
|
||||
{t('feature.tools.modal.variableName.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<input
|
||||
value={localeData.variable || ''}
|
||||
onChange={e => handleValueChange({ variable: e.target.value })}
|
||||
className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none"
|
||||
placeholder={t('feature.tools.modal.variableName.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="mt-6 flex items-center justify-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className="mr-2"
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
showEmojiPicker && (
|
||||
<EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
handleValueChange({ icon, icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
handleValueChange({ icon: '', icon_background: '' })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Modal>
|
||||
</div>
|
||||
{
|
||||
localeData.type === 'api' && (
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
|
||||
{t('apiBasedExtension.selector.title', { ns: 'common' })}
|
||||
<a
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs font-normal text-text-tertiary hover:text-text-accent"
|
||||
>
|
||||
<BookOpen01 className="mr-1 h-3 w-3 text-text-tertiary group-hover:text-text-accent" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
</a>
|
||||
</div>
|
||||
<ApiBasedExtensionSelector
|
||||
value={localeData.config?.api_based_extension_id || ''}
|
||||
onChange={handleDataApiBasedChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="mt-6 flex items-center justify-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className="mr-2"
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
showEmojiPicker && (
|
||||
<EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
handleValueChange({ icon, icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
handleValueChange({ icon: '', icon_background: '' })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
// abandoned
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
@ -15,14 +14,17 @@ import {
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/app/components/base/ui/tooltip'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
|
||||
const Tools = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { setShowExternalDataToolModal } = useModalContext()
|
||||
const {
|
||||
externalDataToolsConfig,
|
||||
@ -48,7 +50,7 @@ const Tools = () => {
|
||||
const promptVariables = modelConfig?.configs?.prompt_variables || []
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable) {
|
||||
notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -66,7 +68,7 @@ const Tools = () => {
|
||||
|
||||
for (let i = 0; i < existedExternalDataTools.length; i++) {
|
||||
if (existedExternalDataTools[i].variable === newExternalDataTool.variable) {
|
||||
notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable }) })
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -110,13 +112,14 @@ const Tools = () => {
|
||||
<div className="mr-1 text-sm font-semibold text-gray-800">
|
||||
{t('feature.tools.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-question-line ml-1 h-4 w-4 shrink-0 text-text-quaternary" />} />
|
||||
<TooltipContent>
|
||||
<div className="max-w-[160px]">
|
||||
{t('feature.tools.tips', { ns: 'appDebug' })}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{
|
||||
!expanded && !!externalDataToolsConfig.length && (
|
||||
@ -151,18 +154,23 @@ const Tools = () => {
|
||||
background={item.icon_background}
|
||||
/>
|
||||
<div className="mr-2 text-[13px] font-medium text-gray-800">{item.label}</div>
|
||||
<Tooltip
|
||||
popupContent={copied ? t('copied', { ns: 'appApi' }) : `${item.variable}, ${t('copy', { ns: 'appApi' })}`}
|
||||
>
|
||||
<div
|
||||
className="text-xs text-gray-500"
|
||||
onClick={() => {
|
||||
copy(item.variable || '')
|
||||
setCopied(true)
|
||||
}}
|
||||
>
|
||||
{item.variable}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div
|
||||
className="text-xs text-gray-500"
|
||||
onClick={() => {
|
||||
copy(item.variable || '')
|
||||
setCopied(true)
|
||||
}}
|
||||
>
|
||||
{item.variable}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{copied ? t('copied', { ns: 'appApi' }) : `${item.variable}, ${t('copy', { ns: 'appApi' })}`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -2,7 +2,6 @@ import type { App } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { MARKETPLACE_URL_PREFIX, NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -57,6 +56,19 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const mockUseRouter = vi.mocked(useRouter)
|
||||
const mockPush = vi.fn()
|
||||
const mockCreateApp = vi.mocked(createApp)
|
||||
@ -79,9 +91,8 @@ const renderModal = () => {
|
||||
const onClose = vi.fn()
|
||||
const onSuccess = vi.fn()
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
|
||||
<CreateAppModal show onClose={onClose} onSuccess={onSuccess} defaultAppMode={AppModeEnum.ADVANCED_CHAT} />
|
||||
</ToastContext.Provider>,
|
||||
<CreateAppModal show onClose={onClose} onSuccess={onSuccess} defaultAppMode={AppModeEnum.ADVANCED_CHAT} />,
|
||||
|
||||
)
|
||||
return { onClose, onSuccess }
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -2,15 +2,13 @@
|
||||
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
@ -58,7 +56,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
const { push } = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [currentFile, setDSLFile] = useState<File | undefined>(droppedFile)
|
||||
const [fileContent, setFileContent] = useState<string>()
|
||||
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||
@ -152,11 +149,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
if (onClose)
|
||||
onClose()
|
||||
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
||||
})
|
||||
toast(t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }), { type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', description: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }) })
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
if (app_id)
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
@ -173,12 +166,12 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
setImportId(id)
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
finally {
|
||||
isCreatingRef.current = false
|
||||
@ -213,22 +206,19 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
if (onClose)
|
||||
onClose()
|
||||
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
})
|
||||
toast.success(t('newApp.appCreated', { ns: 'app' }))
|
||||
if (app_id)
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,94 +255,98 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl"
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
<Dialog
|
||||
open={show}
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary title-2xl-semi-bold">
|
||||
{t('importApp', { ns: 'app' })}
|
||||
<div
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<span className="i-ri-close-line h-[18px] w-[18px] text-text-tertiary" aria-hidden="true" />
|
||||
<DialogContent className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl">
|
||||
<div className="flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary title-2xl-semi-bold">
|
||||
{t('importApp', { ns: 'app' })}
|
||||
<div
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<span className="i-ri-close-line h-[18px] w-[18px] text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary system-md-semibold">
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
'relative flex h-full cursor-pointer items-center',
|
||||
currentTab === tab.key && 'text-text-primary',
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
{currentTab === tab.key && (
|
||||
<div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
{currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
<Uploader
|
||||
className="mt-0"
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
accept=".yaml,.yml,.zip"
|
||||
displayName={isZipFile(currentFile) ? 'ZIP' : 'YAML'}
|
||||
/>
|
||||
)}
|
||||
{currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className="mb-1 text-text-secondary system-md-semibold">
|
||||
{t('importFromDSLUrl', { ns: 'app' })}
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={e => setDslUrlValue(e.target.value)}
|
||||
<div className="flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary system-md-semibold">
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
'relative flex h-full cursor-pointer items-center',
|
||||
currentTab === tab.key && 'text-text-primary',
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
{currentTab === tab.key && (
|
||||
<div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
{currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
<Uploader
|
||||
className="mt-0"
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
accept=".yaml,.yml,.zip"
|
||||
displayName={isZipFile(currentFile) ? 'ZIP' : 'YAML'}
|
||||
/>
|
||||
)}
|
||||
{currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className="mb-1 text-text-secondary system-md-semibold">
|
||||
{t('importFromDSLUrl', { ns: 'app' })}
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={e => setDslUrlValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAppsFull && (
|
||||
<div className="px-6">
|
||||
<AppsFull className="mt-0" loc="app-create-dsl" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAppsFull && (
|
||||
<div className="px-6">
|
||||
<AppsFull className="mt-0" loc="app-create-dsl" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between px-6 pb-6 pt-5">
|
||||
<a
|
||||
className="flex items-center gap-1 text-text-accent system-xs-regular"
|
||||
href={docLink('/use-dify/workspace/app-management#app-export-and-import', appManagementLocalizedPathMap)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{learnMoreLabel}
|
||||
<span className="i-ri-external-link-line h-[12px] w-[12px]" aria-hidden="true" />
|
||||
</a>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('newApp.Cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={buttonDisabled || isCreating}
|
||||
variant="primary"
|
||||
onClick={onCreate}
|
||||
className="gap-1"
|
||||
loading={isCreating}
|
||||
<div className="flex items-center justify-between px-6 pb-6 pt-5">
|
||||
<a
|
||||
className="flex items-center gap-1 text-text-accent system-xs-regular"
|
||||
href={docLink('/use-dify/workspace/app-management#app-export-and-import', appManagementLocalizedPathMap)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>{t('newApp.import', { ns: 'app' })}</span>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
{learnMoreLabel}
|
||||
<span className="i-ri-external-link-line h-[12px] w-[12px]" aria-hidden="true" />
|
||||
</a>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('newApp.Cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={buttonDisabled || isCreating}
|
||||
variant="primary"
|
||||
onClick={onCreate}
|
||||
className="gap-1"
|
||||
loading={isCreating}
|
||||
>
|
||||
<span>{t('newApp.import', { ns: 'app' })}</span>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{showErrorModal && (
|
||||
<DSLConfirmModal
|
||||
file={currentFile}
|
||||
|
||||
@ -7,10 +7,9 @@ import {
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
|
||||
@ -30,7 +29,6 @@ const Uploader: FC<Props> = ({
|
||||
displayName = 'YAML',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
@ -60,7 +58,7 @@ const Uploader: FC<Props> = ({
|
||||
return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) })
|
||||
toast.error(t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }))
|
||||
return
|
||||
}
|
||||
updateFile(files[0])
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ProviderContextState } from '@/context/provider-context'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { baseProviderContextValue } from '@/context/provider-context'
|
||||
import DuplicateAppModal from './index'
|
||||
@ -129,7 +129,7 @@ describe('DuplicateAppModal', () => {
|
||||
|
||||
it('should show error toast when name is empty', async () => {
|
||||
const user = userEvent.setup()
|
||||
const toastSpy = vi.spyOn(Toast, 'notify')
|
||||
const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
// Arrange
|
||||
const { onConfirm, onHide } = renderComponent()
|
||||
|
||||
@ -138,7 +138,7 @@ describe('DuplicateAppModal', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'app.duplicate' }))
|
||||
|
||||
// Assert
|
||||
expect(toastSpy).toHaveBeenCalledWith({ type: 'error', message: 'explore.appCustomize.nameRequired' })
|
||||
expect(toastSpy).toHaveBeenCalledWith('explore.appCustomize.nameRequired')
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
expect(onHide).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -9,7 +9,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -57,7 +57,7 @@ const DuplicateAppModal = ({
|
||||
|
||||
const submit = () => {
|
||||
if (!name.trim()) {
|
||||
Toast.notify({ type: 'error', message: t('appCustomize.nameRequired', { ns: 'explore' }) })
|
||||
toast.error(t('appCustomize.nameRequired', { ns: 'explore' }))
|
||||
return
|
||||
}
|
||||
onConfirm({
|
||||
|
||||
@ -30,8 +30,8 @@ import Drawer from '@/app/components/base/drawer'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import MessageLogModal from '@/app/components/base/message-log-modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -223,7 +223,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
const { formatTime } = useTimestamp()
|
||||
const { onClose, appDetail } = useContext(DrawerContext)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow((state: AppStoreState) => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
@ -413,14 +412,14 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
return item
|
||||
}))
|
||||
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
}, [allChatItems, appDetail?.id, notify, t])
|
||||
}, [allChatItems, appDetail?.id, t])
|
||||
|
||||
const fetchInitiated = useRef(false)
|
||||
|
||||
@ -734,7 +733,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => {
|
||||
// Text Generator App Session Details Including Message List
|
||||
const { data: conversationDetail, refetch: conversationDetailMutate } = useCompletionConversationDetail(appId, conversationId)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleFeedback = async (mid: string, { rating, content }: FeedbackType): Promise<boolean> => {
|
||||
@ -744,11 +742,11 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st
|
||||
body: { message_id: mid, rating, content: content ?? undefined },
|
||||
})
|
||||
conversationDetailMutate()
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -757,11 +755,11 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st
|
||||
try {
|
||||
await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
|
||||
conversationDetailMutate()
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -783,7 +781,6 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st
|
||||
*/
|
||||
const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => {
|
||||
const { data: conversationDetail } = useChatConversationDetail(appId, conversationId)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleFeedback = async (mid: string, { rating, content }: FeedbackType): Promise<boolean> => {
|
||||
@ -792,11 +789,11 @@ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }
|
||||
url: `/apps/${appId}/feedbacks`,
|
||||
body: { message_id: mid, rating, content: content ?? undefined },
|
||||
})
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -804,11 +801,11 @@ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }
|
||||
const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
|
||||
try {
|
||||
await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,19 @@ vi.mock('react-i18next', async () => {
|
||||
})
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => mockNotify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const mockOnClose = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
@ -59,13 +72,6 @@ vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => buildModalContext(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
close: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/context/i18n')>('@/context/i18n')
|
||||
return {
|
||||
|
||||
@ -19,8 +19,8 @@ import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -65,7 +65,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const { notify } = useToastContext()
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const {
|
||||
title,
|
||||
@ -159,7 +158,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
|
||||
const onClickSave = async () => {
|
||||
if (!inputInfo.title) {
|
||||
notify({ type: 'error', message: t('newApp.nameNotEmpty', { ns: 'app' }) })
|
||||
toast.error(t('newApp.nameNotEmpty', { ns: 'app' }))
|
||||
return
|
||||
}
|
||||
|
||||
@ -181,11 +180,11 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
|
||||
if (inputInfo !== null) {
|
||||
if (!validateColorHex(inputInfo.chatColorTheme)) {
|
||||
notify({ type: 'error', message: t(`${prefixSettings}.invalidHexMessage`, { ns: 'appOverview' }) })
|
||||
toast.error(t(`${prefixSettings}.invalidHexMessage`, { ns: 'appOverview' }))
|
||||
return
|
||||
}
|
||||
if (!validatePrivacyPolicy(inputInfo.privacyPolicy)) {
|
||||
notify({ type: 'error', message: t(`${prefixSettings}.invalidPrivacyPolicy`, { ns: 'appOverview' }) })
|
||||
toast.error(t(`${prefixSettings}.invalidPrivacyPolicy`, { ns: 'appOverview' }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -111,20 +110,32 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
|
||||
const renderComponent = (overrides: Partial<React.ComponentProps<typeof SwitchAppModal>> = {}) => {
|
||||
const notify = vi.fn()
|
||||
const mockToast = {
|
||||
success: (message: string, options?: Record<string, unknown>) => notify({ type: 'success', message, ...options }),
|
||||
error: (message: string, options?: Record<string, unknown>) => notify({ type: 'error', message, ...options }),
|
||||
warning: (message: string, options?: Record<string, unknown>) => notify({ type: 'warning', message, ...options }),
|
||||
info: (message: string, options?: Record<string, unknown>) => notify({ type: 'info', message, ...options }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
const onClose = vi.fn()
|
||||
const onSuccess = vi.fn()
|
||||
const appDetail = createMockApp()
|
||||
|
||||
const utils = render(
|
||||
<ToastContext.Provider value={{ notify, close: vi.fn() }}>
|
||||
<SwitchAppModal
|
||||
show
|
||||
appDetail={appDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
{...overrides}
|
||||
/>
|
||||
</ToastContext.Provider>,
|
||||
<SwitchAppModal
|
||||
show
|
||||
appDetail={appDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
{...overrides}
|
||||
/>,
|
||||
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@ -5,7 +5,6 @@ import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
@ -14,7 +13,7 @@ import Confirm from '@/app/components/base/confirm'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -37,7 +36,6 @@ type SwitchAppModalProps = {
|
||||
const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClose }: SwitchAppModalProps) => {
|
||||
const { push, replace } = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
@ -68,7 +66,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) })
|
||||
toast.success(t('newApp.appCreated', { ns: 'app' }))
|
||||
if (inAppDetail)
|
||||
setAppDetail()
|
||||
if (removeOriginal)
|
||||
@ -84,7 +82,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { fetchTextGenerationMessage } from '@/service/debug'
|
||||
import { AppSourceType, fetchMoreLikeThis, submitHumanInputForm, updateFeedback } from '@/service/share'
|
||||
@ -145,7 +145,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
|
||||
const handleMoreLikeThis = async () => {
|
||||
if (isQuerying || !messageId) {
|
||||
Toast.notify({ type: 'warning', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
toast.warning(t('errorMessage.waitForResponse', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
startQuerying()
|
||||
@ -368,7 +368,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
copy(copyContent)
|
||||
else
|
||||
copy(JSON.stringify(copyContent))
|
||||
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
>
|
||||
<RiClipboardLine className="h-4 w-4" />
|
||||
|
||||
@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard'
|
||||
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import SavedItems from './index'
|
||||
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
@ -16,7 +16,7 @@ vi.mock('@/next/navigation', () => ({
|
||||
}))
|
||||
|
||||
const mockCopy = vi.mocked(copy)
|
||||
const toastNotifySpy = vi.spyOn(Toast, 'notify')
|
||||
const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
|
||||
|
||||
const baseProps: ISavedItemsProps = {
|
||||
list: [
|
||||
@ -30,7 +30,7 @@ const baseProps: ISavedItemsProps = {
|
||||
describe('SavedItems', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
toastNotifySpy.mockClear()
|
||||
toastSuccessSpy.mockClear()
|
||||
})
|
||||
|
||||
it('renders saved answers with metadata and controls', () => {
|
||||
@ -58,7 +58,7 @@ describe('SavedItems', () => {
|
||||
|
||||
fireEvent.click(copyButton)
|
||||
expect(mockCopy).toHaveBeenCalledWith('hello world')
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'success', message: 'common.actionMsg.copySuccessfully' })
|
||||
expect(toastSuccessSpy).toHaveBeenCalledWith('common.actionMsg.copySuccessfully')
|
||||
|
||||
fireEvent.click(deleteButton)
|
||||
expect(handleRemove).toHaveBeenCalledWith('1')
|
||||
|
||||
@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import NoData from './no-data'
|
||||
|
||||
@ -60,7 +60,7 @@ const SavedItems: FC<ISavedItemsProps> = ({
|
||||
{isShowTextToSpeech && <NewAudioButton value={answer} />}
|
||||
<ActionButton onClick={() => {
|
||||
copy(answer)
|
||||
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
>
|
||||
<RiClipboardLine className="h-4 w-4" />
|
||||
|
||||
@ -1,24 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||
import type { HtmlContentProps } from '@/app/components/base/popover'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import type { WorkflowOnlineUser } from '@/models/app'
|
||||
import type { App } from '@/types/app'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState, useTransition } from 'react'
|
||||
import { useCallback, useMemo, useState, useTransition } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@ -28,6 +22,17 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@/app/components/base/ui/alert-dialog'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/app/components/base/ui/tooltip'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -65,6 +70,166 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
type AppCardOperationsProps = {
|
||||
app: App
|
||||
webappAuthEnabled: boolean
|
||||
isCurrentWorkspaceEditor: boolean
|
||||
exporting: boolean
|
||||
secretEnvListLength: number
|
||||
isUpgradingRuntime: boolean
|
||||
popupClassName: string
|
||||
onEdit: () => void
|
||||
onDuplicate: () => void
|
||||
onExport: () => void
|
||||
onSwitch: () => void
|
||||
onDelete: () => void
|
||||
onAccessControl: () => void
|
||||
onInstalledApp: () => void
|
||||
onUpgradeRuntime: () => void
|
||||
}
|
||||
|
||||
const AppCardOperations = ({
|
||||
app,
|
||||
webappAuthEnabled,
|
||||
isCurrentWorkspaceEditor,
|
||||
exporting,
|
||||
secretEnvListLength,
|
||||
isUpgradingRuntime,
|
||||
popupClassName,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
onExport,
|
||||
onSwitch,
|
||||
onDelete,
|
||||
onAccessControl,
|
||||
onInstalledApp,
|
||||
onUpgradeRuntime,
|
||||
}: AppCardOperationsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
||||
appId: app.id,
|
||||
enabled: !!open && webappAuthEnabled,
|
||||
})
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClickInstalledApp = async () => {
|
||||
onInstalledApp()
|
||||
onMouseLeave()
|
||||
}
|
||||
|
||||
const onClickUpgradeRuntime = async () => {
|
||||
onUpgradeRuntime()
|
||||
onMouseLeave()
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md',
|
||||
open && '!bg-state-base-hover !shadow-none',
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{t('operation.more', { ns: 'common' })}</span>
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={cn(
|
||||
'w-fit min-w-[130px] overflow-hidden rounded-lg bg-components-panel-bg shadow-lg ring-1 ring-black/5',
|
||||
popupClassName,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onEdit}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('editApp', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onDuplicate}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('duplicate', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<button type="button" disabled={exporting || secretEnvListLength > 0} className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50" onClick={onExport}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('export', { ns: 'app' })}</span>
|
||||
</button>
|
||||
{(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover" onClick={onSwitch}>
|
||||
<span className="text-sm leading-5 text-text-secondary">{t('switch', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
!app.has_draft_trigger && (
|
||||
(!webappAuthEnabled)
|
||||
? (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
<Divider className="my-1" />
|
||||
{
|
||||
webappAuthEnabled && isCurrentWorkspaceEditor && (
|
||||
<>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover" onClick={onAccessControl}>
|
||||
<span className="text-sm leading-5 text-text-secondary">{t('accessControl', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{app.runtime_type !== 'sandboxed'
|
||||
&& (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
&& (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isUpgradingRuntime}
|
||||
className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={onClickUpgradeRuntime}
|
||||
>
|
||||
<span className="text-text-accent system-sm-regular">
|
||||
{t('upgradeRuntime', { ns: 'app' })}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<span className="text-text-secondary system-sm-regular group-hover:text-text-destructive">
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
onRefresh?: () => void
|
||||
@ -73,7 +238,6 @@ export type AppCardProps = {
|
||||
|
||||
const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
@ -93,20 +257,17 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await mutateDeleteApp(app.id)
|
||||
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
|
||||
toast.success(t('appDeleted', { ns: 'app' }))
|
||||
onPlanInfoChanged()
|
||||
}
|
||||
catch (e: unknown) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error ? `: ${e.message}` : ''}`,
|
||||
})
|
||||
toast.error(`${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error ? `: ${e.message}` : ''}`)
|
||||
}
|
||||
finally {
|
||||
setShowConfirmDelete(false)
|
||||
setConfirmDeleteInput('')
|
||||
}
|
||||
}, [app.id, mutateDeleteApp, notify, onPlanInfoChanged, t])
|
||||
}, [app.id, mutateDeleteApp, onPlanInfoChanged, t])
|
||||
|
||||
const onDeleteDialogOpenChange = useCallback((open: boolean) => {
|
||||
if (isDeleting)
|
||||
@ -138,20 +299,14 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
max_active_requests,
|
||||
})
|
||||
setShowEditModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('editDone', { ns: 'app' }),
|
||||
})
|
||||
toast.success(t('editDone', { ns: 'app' }))
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
}
|
||||
catch (e: unknown) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: (e instanceof Error ? e.message : '') || t('editFailed', { ns: 'app' }),
|
||||
})
|
||||
toast.error((e instanceof Error ? e.message : '') || t('editFailed', { ns: 'app' }))
|
||||
}
|
||||
}, [app.id, notify, onRefresh, t])
|
||||
}, [app.id, onRefresh, t])
|
||||
|
||||
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
|
||||
try {
|
||||
@ -164,10 +319,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
mode: app.mode,
|
||||
})
|
||||
setShowDuplicateModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
})
|
||||
toast.success(t('newApp.appCreated', { ns: 'app' }))
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
@ -175,7 +327,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
getRedirection(isCurrentWorkspaceEditor, newApp, push)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,10 +349,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
downloadBlob({ data: file, fileName: `${app.name}.yml` })
|
||||
}
|
||||
catch {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('exportFailed', { ns: 'app' }),
|
||||
})
|
||||
toast.error(t('exportFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,196 +368,71 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
setSecretEnvList(list)
|
||||
}
|
||||
catch {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('exportFailed', { ns: 'app' }),
|
||||
})
|
||||
toast.error(t('exportFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
const [isUpgradingRuntime, startUpgradeRuntime] = useTransition()
|
||||
|
||||
const onSwitch = () => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
setShowSwitchModal(false)
|
||||
}
|
||||
|
||||
const [isUpgradingRuntime, startUpgradeRuntime] = useTransition()
|
||||
const isClassicWorkflowApp = app.runtime_type !== 'sandboxed'
|
||||
&& (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
|
||||
const onUpdateAccessControl = useCallback(() => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
setShowAccessControl(false)
|
||||
}, [onRefresh, setShowAccessControl])
|
||||
|
||||
const Operations = (props: HtmlContentProps) => {
|
||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: app?.id, enabled: (!!props?.open && systemFeatures.webapp_auth.enabled) })
|
||||
const onMouseLeave = async () => {
|
||||
props.onClose?.()
|
||||
}
|
||||
const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowEditModal(true)
|
||||
}
|
||||
const onClickDuplicate = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowDuplicateModal(true)
|
||||
}
|
||||
const onClickExport = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
startExport(async () => {
|
||||
await exportCheck()
|
||||
const handleOpenEditModal = useCallback(() => setShowEditModal(true), [])
|
||||
const handleOpenDuplicateModal = useCallback(() => setShowDuplicateModal(true), [])
|
||||
const handleOpenSwitchModal = useCallback(() => setShowSwitchModal(true), [])
|
||||
const handleOpenDeleteModal = useCallback(() => setShowConfirmDelete(true), [])
|
||||
const handleOpenAccessControl = useCallback(() => setShowAccessControl(true), [])
|
||||
const handleExport = useCallback(() => {
|
||||
startExport(async () => {
|
||||
await exportCheck()
|
||||
})
|
||||
}, [exportCheck, startExport])
|
||||
const handleInstalledApp = useCallback(async () => {
|
||||
try {
|
||||
await openAsyncWindow(async () => {
|
||||
const { installed_apps } = (await fetchInstalledAppList(app.id) || {}) as { installed_apps?: { id: string }[] }
|
||||
if (installed_apps && installed_apps.length > 0)
|
||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||
throw new Error('No app found in Explore')
|
||||
}, {
|
||||
onError: (err) => {
|
||||
toast.error(`${err.message || err}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
const onClickSwitch = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowSwitchModal(true)
|
||||
catch (e: unknown) {
|
||||
toast.error(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
const onClickDelete = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
const onClickAccessControl = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowAccessControl(true)
|
||||
}
|
||||
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
}, [app.id, openAsyncWindow])
|
||||
const handleUpgradeRuntime = useCallback(() => {
|
||||
startUpgradeRuntime(async () => {
|
||||
try {
|
||||
await openAsyncWindow(async () => {
|
||||
const { installed_apps } = (await fetchInstalledAppList(app.id) || {}) as { installed_apps?: { id: string }[] }
|
||||
if (installed_apps && installed_apps.length > 0)
|
||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||
throw new Error('No app found in Explore')
|
||||
}, {
|
||||
onError: (err) => {
|
||||
Toast.notify({ type: 'error', message: `${err.message || err}` })
|
||||
},
|
||||
})
|
||||
const res = await upgradeAppRuntime(app.id)
|
||||
if (res.result === 'success' && res.new_app_id) {
|
||||
toast.success(t('sandboxMigrationModal.upgrade', { ns: 'workflow' }))
|
||||
const params = new URLSearchParams({
|
||||
upgraded_from: app.id,
|
||||
upgraded_from_name: app.name,
|
||||
})
|
||||
push(`/app/${res.new_app_id}/workflow?${params.toString()}`)
|
||||
}
|
||||
}
|
||||
catch (e: unknown) {
|
||||
Toast.notify({ type: 'error', message: e instanceof Error ? e.message : String(e) })
|
||||
toast.error((e instanceof Error ? e.message : '') || 'Upgrade failed')
|
||||
}
|
||||
}
|
||||
const onClickUpgradeRuntime = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
startUpgradeRuntime(async () => {
|
||||
try {
|
||||
const res = await upgradeAppRuntime(app.id)
|
||||
if (res.result === 'success' && res.new_app_id) {
|
||||
notify({ type: 'success', message: t('sandboxMigrationModal.upgrade', { ns: 'workflow' }) })
|
||||
const params = new URLSearchParams({
|
||||
upgraded_from: app.id,
|
||||
upgraded_from_name: app.name,
|
||||
})
|
||||
push(`/app/${res.new_app_id}/workflow?${params.toString()}`)
|
||||
}
|
||||
}
|
||||
catch (e: unknown) {
|
||||
notify({ type: 'error', message: (e instanceof Error ? e.message : '') || 'Upgrade failed' })
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickSettings}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('editApp', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickDuplicate}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('duplicate', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<button type="button" disabled={exporting || secretEnvList.length > 0} className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50" onClick={onClickExport}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('export', { ns: 'app' })}</span>
|
||||
</button>
|
||||
{(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button
|
||||
type="button"
|
||||
className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={onClickSwitch}
|
||||
>
|
||||
<span className="text-sm leading-5 text-text-secondary">{t('switch', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
!app.has_draft_trigger && (
|
||||
(!systemFeatures.webapp_auth.enabled)
|
||||
? (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
<Divider className="my-1" />
|
||||
{
|
||||
systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && (
|
||||
<>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickAccessControl}>
|
||||
<span className="text-sm leading-5 text-text-secondary">{t('accessControl', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{isClassicWorkflowApp && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isUpgradingRuntime}
|
||||
className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={onClickUpgradeRuntime}
|
||||
>
|
||||
<span className="text-text-accent system-sm-regular">{t('upgradeRuntime', { ns: 'app' })}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover"
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
<span className="text-text-secondary system-sm-regular group-hover:text-text-destructive">
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [app.id, app.name, push, startUpgradeRuntime, t])
|
||||
|
||||
const [tags, setTags] = useState<Tag[]>(app.tags)
|
||||
useEffect(() => {
|
||||
setTags(app.tags)
|
||||
}, [app.tags])
|
||||
const [tags, setTags] = useState<Tag[]>(() => app.tags)
|
||||
|
||||
const EditTimeText = useMemo(() => {
|
||||
const timeText = formatTime({
|
||||
@ -463,23 +487,27 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
</div>
|
||||
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
{app.access_mode === AccessMode.PUBLIC && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.anyone', { ns: 'app' })}>
|
||||
<span className="i-ri-global-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-global-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.anyone', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.specific', { ns: 'app' })}>
|
||||
<span className="i-ri-lock-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-lock-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.specific', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.ORGANIZATION && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.organization', { ns: 'app' })}>
|
||||
<span className="i-ri-building-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-building-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.organization', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && (
|
||||
<Tooltip asChild={false} popupContent={t('accessItemsDescription.external', { ns: 'app' })}>
|
||||
<span className="i-ri-verified-badge-line h-4 w-4 text-text-quaternary" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-verified-badge-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.external', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@ -521,29 +549,26 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
</div>
|
||||
<div className="mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex" />
|
||||
<div className="!hidden shrink-0 group-hover:!flex">
|
||||
<CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={(
|
||||
<div
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md"
|
||||
>
|
||||
<span className="sr-only">{t('operation.more', { ns: 'common' })}</span>
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover',
|
||||
)}
|
||||
<AppCardOperations
|
||||
app={app}
|
||||
webappAuthEnabled={systemFeatures.webapp_auth.enabled}
|
||||
isCurrentWorkspaceEditor={isCurrentWorkspaceEditor}
|
||||
exporting={exporting}
|
||||
secretEnvListLength={secretEnvList.length}
|
||||
isUpgradingRuntime={isUpgradingRuntime}
|
||||
popupClassName={
|
||||
(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT)
|
||||
? '!w-[256px] translate-x-[-224px]'
|
||||
: '!w-[216px] translate-x-[-128px]'
|
||||
? 'w-[256px]'
|
||||
: 'w-[216px]'
|
||||
}
|
||||
className="!z-20 h-fit"
|
||||
onEdit={handleOpenEditModal}
|
||||
onDuplicate={handleOpenDuplicateModal}
|
||||
onExport={handleExport}
|
||||
onSwitch={handleOpenSwitchModal}
|
||||
onDelete={handleOpenDeleteModal}
|
||||
onAccessControl={handleOpenAccessControl}
|
||||
onInstalledApp={handleInstalledApp}
|
||||
onUpgradeRuntime={handleUpgradeRuntime}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,16 +1,32 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogDetail from '../detail'
|
||||
|
||||
const { mockToast } = vi.hoisted(() => {
|
||||
const mockToast = Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
})
|
||||
return { mockToast }
|
||||
})
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })),
|
||||
}))
|
||||
@ -22,7 +38,7 @@ vi.mock('@/app/components/workflow/run/status', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
default: ({ title, value }: { title: ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
{title}
|
||||
{typeof value === 'string' ? value : JSON.stringify(value)}
|
||||
@ -76,19 +92,13 @@ const createMockResponse = (overrides: Partial<AgentLogDetailResponse> = {}): Ag
|
||||
})
|
||||
|
||||
describe('AgentLogDetail', () => {
|
||||
const notify = vi.fn()
|
||||
|
||||
const renderComponent = (props: Partial<ComponentProps<typeof AgentLogDetail>> = {}) => {
|
||||
const defaultProps: ComponentProps<typeof AgentLogDetail> = {
|
||||
conversationID: 'conv-id',
|
||||
messageID: 'msg-id',
|
||||
log: createMockLog(),
|
||||
}
|
||||
return render(
|
||||
<ToastContext.Provider value={{ notify, close: vi.fn() } as ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogDetail {...defaultProps} {...props} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
return render(<AgentLogDetail {...defaultProps} {...props} />)
|
||||
}
|
||||
|
||||
const renderAndWaitForData = async (props: Partial<ComponentProps<typeof AgentLogDetail>> = {}) => {
|
||||
@ -212,10 +222,7 @@ describe('AgentLogDetail', () => {
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Error: API Error',
|
||||
})
|
||||
expect(mockToast.error).toHaveBeenCalledWith('Error: API Error')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,14 +1,30 @@
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogModal from '../index'
|
||||
|
||||
const { mockToast } = vi.hoisted(() => {
|
||||
const mockToast = Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
})
|
||||
return { mockToast }
|
||||
})
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })),
|
||||
}))
|
||||
@ -94,11 +110,7 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should render correctly when log item is provided', async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
|
||||
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
|
||||
|
||||
@ -110,11 +122,7 @@ describe('AgentLogModal', () => {
|
||||
it('should call onCancel when close button is clicked', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
|
||||
const closeBtn = screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }).nextElementSibling!
|
||||
fireEvent.click(closeBtn)
|
||||
@ -130,11 +138,7 @@ describe('AgentLogModal', () => {
|
||||
clickAwayHandler = callback
|
||||
})
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
clickAwayHandler(new Event('click'))
|
||||
|
||||
expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
@ -150,11 +154,7 @@ describe('AgentLogModal', () => {
|
||||
}
|
||||
})
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
|
||||
expect(mockProps.onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -7,10 +7,9 @@ import { flatten } from 'es-toolkit/compat'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ResultPanel from './result'
|
||||
@ -22,28 +21,19 @@ export type AgentLogDetailProps = {
|
||||
log: IChatItem
|
||||
messageID: string
|
||||
}
|
||||
|
||||
const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
activeTab = 'DETAIL',
|
||||
conversationID,
|
||||
messageID,
|
||||
log,
|
||||
}) => {
|
||||
const AgentLogDetail: FC<AgentLogDetailProps> = ({ activeTab = 'DETAIL', conversationID, messageID, log }) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [currentTab, setCurrentTab] = useState<string>(activeTab)
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [runDetail, setRunDetail] = useState<AgentLogDetailResponse>()
|
||||
const [list, setList] = useState<AgentIteration[]>([])
|
||||
|
||||
const tools = useMemo(() => {
|
||||
const res = uniq(flatten(runDetail?.iterations.map((iteration) => {
|
||||
return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean)
|
||||
})).filter(Boolean))
|
||||
return res
|
||||
}, [runDetail])
|
||||
|
||||
const getLogDetail = useCallback(async (appID: string, conversationID: string, messageID: string) => {
|
||||
try {
|
||||
const res = await fetchAgentLogDetail({
|
||||
@ -57,51 +47,30 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
setList(res.iterations)
|
||||
}
|
||||
catch (err) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${err}`,
|
||||
})
|
||||
toast.error(`${err}`)
|
||||
}
|
||||
}, [notify])
|
||||
|
||||
}, [])
|
||||
const getData = async (appID: string, conversationID: string, messageID: string) => {
|
||||
setLoading(true)
|
||||
await getLogDetail(appID, conversationID, messageID)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const switchTab = async (tab: string) => {
|
||||
setCurrentTab(tab)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// fetch data
|
||||
if (appDetail)
|
||||
getData(appDetail.id, conversationID, messageID)
|
||||
}, [appDetail, conversationID, messageID])
|
||||
|
||||
return (
|
||||
<div className="relative flex grow flex-col">
|
||||
{/* tab */}
|
||||
<div className="flex shrink-0 items-center border-b-[0.5px] border-divider-regular px-4">
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
|
||||
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary',
|
||||
)}
|
||||
data-active={currentTab === 'DETAIL'}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>
|
||||
<div className={cn('mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary', currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary')} data-active={currentTab === 'DETAIL'} onClick={() => switchTab('DETAIL')}>
|
||||
{t('detail', { ns: 'runLog' })}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
|
||||
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary',
|
||||
)}
|
||||
data-active={currentTab === 'TRACING'}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
>
|
||||
<div className={cn('mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary', currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary')} data-active={currentTab === 'TRACING'} onClick={() => switchTab('TRACING')}>
|
||||
{t('tracing', { ns: 'runLog' })}
|
||||
</div>
|
||||
</div>
|
||||
@ -112,29 +81,10 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{!loading && currentTab === 'DETAIL' && runDetail && (
|
||||
<ResultPanel
|
||||
inputs={log.input}
|
||||
outputs={log.content}
|
||||
status={runDetail.meta.status}
|
||||
error={runDetail.meta.error}
|
||||
elapsed_time={runDetail.meta.elapsed_time}
|
||||
total_tokens={runDetail.meta.total_tokens}
|
||||
created_at={runDetail.meta.start_time}
|
||||
created_by={runDetail.meta.executor}
|
||||
agentMode={runDetail.meta.agent_mode}
|
||||
tools={tools}
|
||||
iterations={runDetail.iterations.length}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'TRACING' && (
|
||||
<TracingPanel
|
||||
list={list}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'DETAIL' && runDetail && (<ResultPanel inputs={log.input} outputs={log.content} status={runDetail.meta.status} error={runDetail.meta.error} elapsed_time={runDetail.meta.elapsed_time} total_tokens={runDetail.meta.total_tokens} created_at={runDetail.meta.start_time} created_by={runDetail.meta.executor} agentMode={runDetail.meta.agent_mode} tools={tools} iterations={runDetail.iterations.length} />)}
|
||||
{!loading && currentTab === 'TRACING' && (<TracingPanel list={list} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogDetail
|
||||
|
||||
@ -3,7 +3,7 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import { ToastHost } from '@/app/components/base/ui/toast'
|
||||
import AgentLogModal from '.'
|
||||
|
||||
const MOCK_RESPONSE: AgentLogDetailResponse = {
|
||||
@ -109,7 +109,8 @@ const AgentLogModalDemo = ({
|
||||
}, [setAppDetail])
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<>
|
||||
<ToastHost />
|
||||
<div className="relative min-h-[540px] w-full bg-background-default-subtle p-6">
|
||||
<AgentLogModal
|
||||
currentLogItem={MOCK_CHAT_ITEM}
|
||||
@ -119,7 +120,7 @@ const AgentLogModalDemo = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import AudioPlayer from '../audio'
|
||||
const mockToastNotify = vi.hoisted(() => vi.fn())
|
||||
const mockTextToAudioStream = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
default: {
|
||||
notify: (...args: unknown[]) => mockToastNotify(...args),
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { AppSourceType, textToAudioStream } from '@/service/share'
|
||||
|
||||
declare global {
|
||||
@ -7,7 +7,6 @@ declare global {
|
||||
ManagedMediaSource: any
|
||||
}
|
||||
}
|
||||
|
||||
export default class AudioPlayer {
|
||||
mediaSource: MediaSource | null
|
||||
audio: HTMLAudioElement
|
||||
@ -22,7 +21,6 @@ export default class AudioPlayer {
|
||||
url: string
|
||||
isPublic: boolean
|
||||
callback: ((event: string) => void) | null
|
||||
|
||||
constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => void) | null) {
|
||||
this.audioContext = new AudioContext()
|
||||
this.msgId = msgId
|
||||
@ -31,14 +29,10 @@ export default class AudioPlayer {
|
||||
this.isPublic = isPublic
|
||||
this.voice = voice
|
||||
this.callback = callback
|
||||
|
||||
// Compatible with iphone ios17 ManagedMediaSource
|
||||
const MediaSource = window.ManagedMediaSource || window.MediaSource
|
||||
if (!MediaSource) {
|
||||
Toast.notify({
|
||||
message: 'Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.',
|
||||
type: 'error',
|
||||
})
|
||||
toast.error('Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.')
|
||||
}
|
||||
this.mediaSource = MediaSource ? new MediaSource() : null
|
||||
this.audio = new Audio()
|
||||
@ -49,7 +43,6 @@ export default class AudioPlayer {
|
||||
}
|
||||
this.audio.src = this.mediaSource ? URL.createObjectURL(this.mediaSource) : ''
|
||||
this.audio.autoplay = true
|
||||
|
||||
const source = this.audioContext.createMediaElementSource(this.audio)
|
||||
source.connect(this.audioContext.destination)
|
||||
this.listenMediaSource('audio/mpeg')
|
||||
@ -63,7 +56,6 @@ export default class AudioPlayer {
|
||||
this.mediaSource?.addEventListener('sourceopen', () => {
|
||||
if (this.sourceBuffer)
|
||||
return
|
||||
|
||||
this.sourceBuffer = this.mediaSource?.addSourceBuffer(contentType)
|
||||
})
|
||||
}
|
||||
@ -106,22 +98,18 @@ export default class AudioPlayer {
|
||||
voice: this.voice,
|
||||
text: this.msgContent,
|
||||
})
|
||||
|
||||
if (audioResponse.status !== 200) {
|
||||
this.isLoadData = false
|
||||
if (this.callback)
|
||||
this.callback('error')
|
||||
}
|
||||
|
||||
const reader = audioResponse.body.getReader()
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
|
||||
if (done) {
|
||||
this.receiveAudioData(value)
|
||||
break
|
||||
}
|
||||
|
||||
this.receiveAudioData(value)
|
||||
}
|
||||
}
|
||||
@ -167,7 +155,6 @@ export default class AudioPlayer {
|
||||
this.theEndOfStream()
|
||||
clearInterval(timer)
|
||||
}
|
||||
|
||||
if (this.cacheBuffers.length && !this.sourceBuffer?.updating) {
|
||||
const arrayBuffer = this.cacheBuffers.shift()!
|
||||
this.sourceBuffer?.appendBuffer(arrayBuffer)
|
||||
@ -180,7 +167,6 @@ export default class AudioPlayer {
|
||||
this.finishStream()
|
||||
return
|
||||
}
|
||||
|
||||
const audioContent = Buffer.from(audio, 'base64')
|
||||
this.receiveAudioData(new Uint8Array(audioContent))
|
||||
if (play) {
|
||||
@ -196,7 +182,6 @@ export default class AudioPlayer {
|
||||
this.callback?.('play')
|
||||
}
|
||||
else if (this.audio.played) { /* empty */ }
|
||||
|
||||
else {
|
||||
this.audio.play()
|
||||
this.callback?.('play')
|
||||
@ -221,7 +206,6 @@ export default class AudioPlayer {
|
||||
this.finishStream()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.sourceBuffer?.updating) {
|
||||
this.cacheBuffers.push(audioData)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { t } from 'i18next'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -10,7 +10,6 @@ type AudioPlayerProps = {
|
||||
src?: string // Keep backward compatibility
|
||||
srcs?: string[] // Support multiple sources
|
||||
}
|
||||
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
@ -23,43 +22,34 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const [hoverTime, setHoverTime] = useState(0)
|
||||
const [isAudioAvailable, setIsAudioAvailable] = useState(true)
|
||||
const { theme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
/* v8 ignore next 2 - @preserve */
|
||||
if (!audio)
|
||||
return
|
||||
|
||||
const handleError = () => {
|
||||
setIsAudioAvailable(false)
|
||||
}
|
||||
|
||||
const setAudioData = () => {
|
||||
setDuration(audio.duration)
|
||||
}
|
||||
|
||||
const setAudioTime = () => {
|
||||
setCurrentTime(audio.currentTime)
|
||||
}
|
||||
|
||||
const handleProgress = () => {
|
||||
if (audio.buffered.length > 0)
|
||||
setBufferedTime(audio.buffered.end(audio.buffered.length - 1))
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false)
|
||||
}
|
||||
|
||||
audio.addEventListener('loadedmetadata', setAudioData)
|
||||
audio.addEventListener('timeupdate', setAudioTime)
|
||||
audio.addEventListener('progress', handleProgress)
|
||||
audio.addEventListener('ended', handleEnded)
|
||||
audio.addEventListener('error', handleError)
|
||||
|
||||
// Preload audio metadata
|
||||
audio.load()
|
||||
|
||||
// Use the first source or src to generate waveform
|
||||
const primarySrc = srcs?.[0] || src
|
||||
if (primarySrc) {
|
||||
@ -76,17 +66,12 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
}
|
||||
}
|
||||
}, [src, srcs])
|
||||
|
||||
const generateWaveformData = async (audioSrc: string) => {
|
||||
if (!window.AudioContext && !(window as any).webkitAudioContext) {
|
||||
setIsAudioAvailable(false)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Web Audio API is not supported in this browser',
|
||||
})
|
||||
toast.error('Web Audio API is not supported in this browser')
|
||||
return null
|
||||
}
|
||||
|
||||
const primarySrc = srcs?.[0] || src
|
||||
const url = primarySrc ? new URL(primarySrc) : null
|
||||
const isHttp = url ? (url.protocol === 'http:' || url.protocol === 'https:') : false
|
||||
@ -94,53 +79,43 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
setIsAudioAvailable(false)
|
||||
return null
|
||||
}
|
||||
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
const samples = 70
|
||||
|
||||
try {
|
||||
const response = await fetch(audioSrc, { mode: 'cors' })
|
||||
if (!response || !response.ok) {
|
||||
setIsAudioAvailable(false)
|
||||
return null
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
|
||||
const channelData = audioBuffer.getChannelData(0)
|
||||
const blockSize = Math.floor(channelData.length / samples)
|
||||
const waveformData: number[] = []
|
||||
|
||||
for (let i = 0; i < samples; i++) {
|
||||
let sum = 0
|
||||
for (let j = 0; j < blockSize; j++)
|
||||
sum += Math.abs(channelData[i * blockSize + j])
|
||||
|
||||
// Apply nonlinear scaling to enhance small amplitudes
|
||||
waveformData.push((sum / blockSize) * 5)
|
||||
}
|
||||
|
||||
// Normalized waveform data
|
||||
const maxAmplitude = Math.max(...waveformData)
|
||||
const normalizedWaveform = waveformData.map(amp => amp / maxAmplitude)
|
||||
|
||||
setWaveformData(normalizedWaveform)
|
||||
setIsAudioAvailable(true)
|
||||
}
|
||||
catch {
|
||||
const waveform: number[] = []
|
||||
let prevValue = Math.random()
|
||||
|
||||
for (let i = 0; i < samples; i++) {
|
||||
const targetValue = Math.random()
|
||||
const interpolatedValue = prevValue + (targetValue - prevValue) * 0.3
|
||||
waveform.push(interpolatedValue)
|
||||
prevValue = interpolatedValue
|
||||
}
|
||||
|
||||
const maxAmplitude = Math.max(...waveform)
|
||||
const randomWaveform = waveform.map(amp => amp / maxAmplitude)
|
||||
|
||||
setWaveformData(randomWaveform)
|
||||
setIsAudioAvailable(true)
|
||||
}
|
||||
@ -148,7 +123,6 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
await audioContext.close()
|
||||
}
|
||||
}
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
const audio = audioRef.current
|
||||
if (audio && isAudioAvailable) {
|
||||
@ -160,99 +134,75 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
setHasStartedPlaying(true)
|
||||
audio.play().catch(error => console.error('Error playing audio:', error))
|
||||
}
|
||||
|
||||
setIsPlaying(!isPlaying)
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Audio element not found',
|
||||
})
|
||||
toast.error('Audio element not found')
|
||||
setIsAudioAvailable(false)
|
||||
}
|
||||
}, [isAudioAvailable, isPlaying])
|
||||
|
||||
const handleCanvasInteraction = useCallback((e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const getClientX = (event: React.MouseEvent | React.TouchEvent): number => {
|
||||
if ('touches' in event)
|
||||
return event.touches[0].clientX
|
||||
return event.clientX
|
||||
}
|
||||
|
||||
const updateProgress = (clientX: number) => {
|
||||
const canvas = canvasRef.current
|
||||
const audio = audioRef.current
|
||||
if (!canvas || !audio)
|
||||
return
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width
|
||||
const newTime = percent * duration
|
||||
|
||||
// Removes the buffer check, allowing drag to any location
|
||||
audio.currentTime = newTime
|
||||
setCurrentTime(newTime)
|
||||
|
||||
if (!isPlaying) {
|
||||
setIsPlaying(true)
|
||||
audio.play().catch((error) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `Error playing audio: ${error}`,
|
||||
})
|
||||
toast.error(`Error playing audio: ${error}`)
|
||||
setIsPlaying(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(getClientX(e))
|
||||
}, [duration, isPlaying])
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const drawWaveform = useCallback(() => {
|
||||
const canvas = canvasRef.current
|
||||
/* v8 ignore next 2 - @preserve */
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
const width = canvas.width
|
||||
const height = canvas.height
|
||||
const data = waveformData
|
||||
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
const barWidth = width / data.length
|
||||
const playedWidth = (currentTime / duration) * width
|
||||
const cornerRadius = 2
|
||||
|
||||
// Draw waveform bars
|
||||
data.forEach((value, index) => {
|
||||
let color
|
||||
|
||||
if (index * barWidth <= playedWidth)
|
||||
color = theme === Theme.light ? '#296DFF' : '#84ABFF'
|
||||
else if ((index * barWidth / width) * duration <= hoverTime)
|
||||
color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
|
||||
else
|
||||
color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'
|
||||
|
||||
const barHeight = value * height
|
||||
const rectX = index * barWidth
|
||||
const rectY = (height - barHeight) / 2
|
||||
const rectWidth = barWidth * 0.5
|
||||
const rectHeight = barHeight
|
||||
|
||||
ctx.lineWidth = 1
|
||||
ctx.fillStyle = color
|
||||
if (ctx.roundRect) {
|
||||
@ -265,27 +215,22 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
}
|
||||
})
|
||||
}, [currentTime, duration, hoverTime, theme, waveformData])
|
||||
|
||||
useEffect(() => {
|
||||
drawWaveform()
|
||||
}, [drawWaveform, bufferedTime, hasStartedPlaying])
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current
|
||||
const audio = audioRef.current
|
||||
if (!canvas || !audio)
|
||||
return
|
||||
|
||||
const clientX = 'touches' in e
|
||||
? e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX
|
||||
: e.clientX
|
||||
if (clientX === undefined)
|
||||
return
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width
|
||||
const time = percent * duration
|
||||
|
||||
// Check if the hovered position is within a buffered range before updating hoverTime
|
||||
for (let i = 0; i < audio.buffered.length; i++) {
|
||||
if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) {
|
||||
@ -294,38 +239,20 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
}
|
||||
}
|
||||
}, [duration])
|
||||
|
||||
return (
|
||||
<div className="flex h-9 min-w-[240px] max-w-[420px] items-center gap-2 rounded-[10px] border border-components-panel-border-subtle bg-components-chat-input-audio-bg-alt p-2 shadow-xs backdrop-blur-sm">
|
||||
<audio ref={audioRef} src={src} preload="auto" data-testid="audio-player">
|
||||
{/* If srcs array is provided, render multiple source elements */}
|
||||
{srcs && srcs.map((srcUrl, index) => (
|
||||
<source key={index} src={srcUrl} />
|
||||
))}
|
||||
{srcs && srcs.map((srcUrl, index) => (<source key={index} src={srcUrl} />))}
|
||||
</audio>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="play-pause-btn"
|
||||
className="inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled"
|
||||
onClick={togglePlay}
|
||||
disabled={!isAudioAvailable}
|
||||
>
|
||||
<button type="button" data-testid="play-pause-btn" className="inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled" onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
{isPlaying
|
||||
? (<div className="i-ri-pause-circle-fill h-5 w-5" />)
|
||||
: (<div className="i-ri-play-large-fill h-5 w-5" />)}
|
||||
</button>
|
||||
<div className={cn(isAudioAvailable && 'grow')} hidden={!isAudioAvailable}>
|
||||
<div className="flex h-8 items-center justify-center">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
data-testid="waveform-canvas"
|
||||
className="relative flex h-6 w-full grow cursor-pointer items-center justify-center"
|
||||
onClick={handleCanvasInteraction}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseDown={handleCanvasInteraction}
|
||||
onTouchMove={handleMouseMove}
|
||||
onTouchStart={handleCanvasInteraction}
|
||||
/>
|
||||
<canvas ref={canvasRef} data-testid="waveform-canvas" className="relative flex h-6 w-full grow cursor-pointer items-center justify-center" onClick={handleCanvasInteraction} onMouseMove={handleMouseMove} onMouseDown={handleCanvasInteraction} onTouchMove={handleMouseMove} onTouchStart={handleCanvasInteraction} />
|
||||
<div className="inline-flex min-w-[50px] items-center justify-center text-text-accent-secondary system-xs-medium">
|
||||
<span className="rounded-[10px] px-0.5 py-1">{formatTime(duration)}</span>
|
||||
</div>
|
||||
@ -335,5 +262,4 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AudioPlayer
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { ToastHandle } from '@/app/components/base/toast'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import useThemeMock from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import AudioPlayer from '../AudioPlayer'
|
||||
@ -263,10 +262,12 @@ describe('AudioPlayer — waveform generation', () => {
|
||||
|
||||
it('should show Toast when AudioContext is not available', async () => {
|
||||
vi.stubGlobal('AudioContext', undefined)
|
||||
const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
|
||||
render(<AudioPlayer src="https://example.com/audio.mp3" />)
|
||||
await advanceWaveformTimer()
|
||||
|
||||
expect(toastSpy).toHaveBeenCalledWith('Web Audio API is not supported in this browser')
|
||||
const toastFound = Array.from(document.body.querySelectorAll('div')).some(
|
||||
d => d.textContent?.includes('Web Audio API is not supported in this browser'),
|
||||
)
|
||||
@ -529,7 +530,7 @@ describe('AudioPlayer — missing coverage', () => {
|
||||
|
||||
it('should keep play button disabled when source is unavailable', async () => {
|
||||
vi.stubGlobal('AudioContext', buildAudioContext(300))
|
||||
const toastSpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({} as unknown as ToastHandle))
|
||||
const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
render(<AudioPlayer src="blob:https://example.com" />)
|
||||
await advanceWaveformTimer() // sets isAudioAvailable to false (invalid protocol)
|
||||
|
||||
@ -545,7 +546,7 @@ describe('AudioPlayer — missing coverage', () => {
|
||||
})
|
||||
|
||||
it('should notify when toggle is invoked while audio is unavailable', async () => {
|
||||
const toastSpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({} as unknown as ToastHandle))
|
||||
const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
const audio = document.querySelector('audio') as HTMLAudioElement
|
||||
await act(async () => {
|
||||
@ -559,10 +560,7 @@ describe('AudioPlayer — missing coverage', () => {
|
||||
props.onClick?.()
|
||||
})
|
||||
|
||||
expect(toastSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
message: 'Audio element not found',
|
||||
}))
|
||||
expect(toastSpy).toHaveBeenCalledWith('Audio element not found')
|
||||
toastSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@ -626,7 +624,7 @@ describe('AudioPlayer — additional branch coverage', () => {
|
||||
})
|
||||
|
||||
it('should ignore toggle click after audio error marks source unavailable', async () => {
|
||||
const toastSpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({} as unknown as ToastHandle))
|
||||
const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
const audio = document.querySelector('audio') as HTMLAudioElement
|
||||
await act(async () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import BlockInput, { getInputKeys } from '../index'
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
@ -14,7 +14,7 @@ vi.mock('@/utils/var', () => ({
|
||||
describe('BlockInput', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(Toast, 'notify')
|
||||
vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
cleanup()
|
||||
})
|
||||
|
||||
@ -138,7 +138,7 @@ describe('BlockInput', () => {
|
||||
fireEvent.change(textarea, { target: { value: '{{invalid}}' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalled()
|
||||
expect(toast.error).toHaveBeenCalled()
|
||||
})
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import VarHighlight from '../../app/configuration/base/var-highlight'
|
||||
import Toast from '../toast'
|
||||
|
||||
// regex to match the {{}} and replace it with a span
|
||||
const regex = /\{\{([^}]+)\}\}/g
|
||||
|
||||
export const getInputKeys = (value: string) => {
|
||||
const keys = value.match(regex)?.map((item) => {
|
||||
return item.replace('{{', '').replace('}}', '')
|
||||
@ -22,13 +19,11 @@ export const getInputKeys = (value: string) => {
|
||||
keys.forEach((key) => {
|
||||
if (keyObj[key])
|
||||
return
|
||||
|
||||
keyObj[key] = true
|
||||
res.push(key)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export type IBlockInputProps = {
|
||||
value: string
|
||||
className?: string // wrapper class
|
||||
@ -36,20 +31,13 @@ export type IBlockInputProps = {
|
||||
readonly?: boolean
|
||||
onConfirm?: (value: string, keys: string[]) => void
|
||||
}
|
||||
|
||||
const BlockInput: FC<IBlockInputProps> = ({
|
||||
value = '',
|
||||
className,
|
||||
readonly = false,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const BlockInput: FC<IBlockInputProps> = ({ value = '', className, readonly = false, onConfirm }) => {
|
||||
const { t } = useTranslation()
|
||||
// current is used to store the current value of the contentEditable element
|
||||
const [currentValue, setCurrentValue] = useState<string>(value)
|
||||
useEffect(() => {
|
||||
setCurrentValue(value)
|
||||
}, [value])
|
||||
|
||||
const contentEditableRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false)
|
||||
useEffect(() => {
|
||||
@ -57,57 +45,42 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
// TODO: Focus at the click position
|
||||
if (currentValue)
|
||||
contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
|
||||
|
||||
contentEditableRef.current.focus()
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
const style = cn({
|
||||
'block h-full w-full break-all border-0 px-4 py-2 text-sm text-gray-900 outline-0': true,
|
||||
'block-input--editing': isEditing,
|
||||
})
|
||||
|
||||
const renderSafeContent = (value: string) => {
|
||||
const parts = value.split(/(\{\{[^}]+\}\}|\n)/g)
|
||||
return parts.map((part, index) => {
|
||||
const variableMatch = /^\{\{([^}]+)\}\}$/.exec(part)
|
||||
if (variableMatch) {
|
||||
return (
|
||||
<VarHighlight
|
||||
key={`var-${index}`}
|
||||
name={variableMatch[1]}
|
||||
/>
|
||||
)
|
||||
return (<VarHighlight key={`var-${index}`} name={variableMatch[1]} />)
|
||||
}
|
||||
if (part === '\n')
|
||||
return <br key={`br-${index}`} />
|
||||
|
||||
return <span key={`text-${index}`}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
// Not use useCallback. That will cause out callback get old data.
|
||||
const handleSubmit = (value: string) => {
|
||||
if (onConfirm) {
|
||||
const keys = getInputKeys(value)
|
||||
const result = checkKeys(keys)
|
||||
if (!result.isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }),
|
||||
})
|
||||
toast.error(t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }))
|
||||
return
|
||||
}
|
||||
onConfirm(value, keys)
|
||||
}
|
||||
}
|
||||
|
||||
const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
setCurrentValue(value)
|
||||
handleSubmit(value)
|
||||
}, [])
|
||||
|
||||
// Prevent rerendering caused cursor to jump to the start of the contentEditable element
|
||||
const TextAreaContentView = () => {
|
||||
return (
|
||||
@ -116,10 +89,8 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const placeholder = ''
|
||||
const editAreaClassName = 'focus:outline-none bg-transparent text-sm'
|
||||
|
||||
const textAreaContent = (
|
||||
<div className={cn(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', 'overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}>
|
||||
{isEditing
|
||||
@ -134,10 +105,10 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
onBlur={() => {
|
||||
blur()
|
||||
setIsEditing(false)
|
||||
// click confirm also make blur. Then outer value is change. So below code has problem.
|
||||
// setTimeout(() => {
|
||||
// handleCancel()
|
||||
// }, 1000)
|
||||
// click confirm also make blur. Then outer value is change. So below code has problem.
|
||||
// setTimeout(() => {
|
||||
// handleCancel()
|
||||
// }, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -145,7 +116,6 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
: <TextAreaContentView />}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn('block-input w-full overflow-y-auto rounded-xl border-none bg-white')} data-testid="block-input">
|
||||
{textAreaContent}
|
||||
@ -159,5 +129,4 @@ const BlockInput: FC<IBlockInputProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(BlockInput)
|
||||
|
||||
@ -4,7 +4,7 @@ import type { InstalledApp } from '@/models/explore'
|
||||
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import { ToastHost } from '@/app/components/base/ui/toast'
|
||||
import {
|
||||
AppSourceType,
|
||||
delConversation,
|
||||
@ -95,7 +95,8 @@ const createQueryClient = () => new QueryClient({
|
||||
const createWrapper = (queryClient: QueryClient) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
<ToastHost />
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,47 +1,21 @@
|
||||
import type { ExtraContent } from '../chat/type'
|
||||
import type {
|
||||
Callback,
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import type { Callback, ChatConfig, ChatItem, Feedback } from '../types'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import type {
|
||||
AppData,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import type { AppData, ConversationItem } from '@/models/share'
|
||||
import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import {
|
||||
AppSourceType,
|
||||
delConversation,
|
||||
pinConversation,
|
||||
renameConversation,
|
||||
unpinConversation,
|
||||
updateFeedback,
|
||||
} from '@/service/share'
|
||||
import {
|
||||
useInvalidateShareConversations,
|
||||
useShareChatList,
|
||||
useShareConversationName,
|
||||
useShareConversations,
|
||||
} from '@/service/use-share'
|
||||
import { AppSourceType, delConversation, pinConversation, renameConversation, unpinConversation, updateFeedback } from '@/service/share'
|
||||
import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
@ -94,14 +68,12 @@ function getFormattedChatList(messages: any[]) {
|
||||
})
|
||||
return newChatList
|
||||
}
|
||||
|
||||
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
const appInfo = useWebAppStore(s => s.appInfo)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const appMeta = useWebAppStore(s => s.appMeta)
|
||||
|
||||
useAppFavicon({
|
||||
enable: !installedAppInfo,
|
||||
icon_type: appInfo?.site.icon_type,
|
||||
@ -109,7 +81,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
icon_background: appInfo?.site.icon_background,
|
||||
icon_url: appInfo?.site.icon_url,
|
||||
})
|
||||
|
||||
const appData = useMemo(() => {
|
||||
if (isInstalledApp) {
|
||||
const { id, app } = installedAppInfo!
|
||||
@ -130,18 +101,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
custom_config: null,
|
||||
} as AppData
|
||||
}
|
||||
|
||||
return appInfo
|
||||
}, [isInstalledApp, installedAppInfo, appInfo])
|
||||
const appId = useMemo(() => appData?.app_id, [appData])
|
||||
|
||||
const [userId, setUserId] = useState<string>()
|
||||
useEffect(() => {
|
||||
getProcessedSystemVariablesFromUrlParams().then(({ user_id }) => {
|
||||
setUserId(user_id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const setLocaleFromProps = async () => {
|
||||
if (appData?.site.default_language)
|
||||
@ -149,7 +117,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}
|
||||
setLocaleFromProps()
|
||||
}, [appData])
|
||||
|
||||
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
@ -193,15 +160,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
})
|
||||
}
|
||||
}, [appId, conversationIdInfo, setConversationIdInfo, userId])
|
||||
|
||||
const [newConversationId, setNewConversationId] = useState('')
|
||||
const chatShouldReloadKey = useMemo(() => {
|
||||
if (currentConversationId === newConversationId)
|
||||
return ''
|
||||
|
||||
return currentConversationId
|
||||
}, [currentConversationId, newConversationId])
|
||||
|
||||
const { data: appPinnedConversationData } = useShareConversations({
|
||||
appSourceType,
|
||||
appId,
|
||||
@ -212,10 +176,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
const {
|
||||
data: appConversationData,
|
||||
isLoading: appConversationDataLoading,
|
||||
} = useShareConversations({
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading } = useShareConversations({
|
||||
appSourceType,
|
||||
appId,
|
||||
pinned: false,
|
||||
@ -225,10 +186,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
const {
|
||||
data: appChatListData,
|
||||
isLoading: appChatListDataLoading,
|
||||
} = useShareChatList({
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useShareChatList({
|
||||
conversationId: chatShouldReloadKey,
|
||||
appSourceType,
|
||||
appId,
|
||||
@ -238,18 +196,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
const invalidateShareConversations = useInvalidateShareConversations()
|
||||
|
||||
const [clearChatList, setClearChatList] = useState(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const appPrevChatTree = useMemo(
|
||||
() => (currentConversationId && appChatListData?.data.length)
|
||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||
: [],
|
||||
[appChatListData, currentConversationId],
|
||||
)
|
||||
|
||||
const appPrevChatTree = useMemo(() => (currentConversationId && appChatListData?.data.length)
|
||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||
: [], [appChatListData, currentConversationId])
|
||||
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
||||
|
||||
const pinnedConversationList = useMemo(() => {
|
||||
return appPinnedConversationData?.data || []
|
||||
}, [appPinnedConversationData])
|
||||
@ -268,7 +220,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
let value = initInputs[item.paragraph.variable]
|
||||
if (value && item.paragraph.max_length && value.length > item.paragraph.max_length)
|
||||
value = value.slice(0, item.paragraph.max_length)
|
||||
|
||||
return {
|
||||
...item.paragraph,
|
||||
default: value || item.default || item.paragraph.default,
|
||||
@ -283,7 +234,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
type: 'number',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.checkbox) {
|
||||
const preset = initInputs[item.checkbox.variable] === true
|
||||
return {
|
||||
@ -292,7 +242,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.select) {
|
||||
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
|
||||
return {
|
||||
@ -301,32 +250,27 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
type: 'select',
|
||||
}
|
||||
}
|
||||
|
||||
if (item['file-list']) {
|
||||
return {
|
||||
...item['file-list'],
|
||||
type: 'file-list',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.file) {
|
||||
return {
|
||||
...item.file,
|
||||
type: 'file',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.json_object) {
|
||||
return {
|
||||
...item.json_object,
|
||||
type: 'json_object',
|
||||
}
|
||||
}
|
||||
|
||||
let value = initInputs[item['text-input'].variable]
|
||||
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
|
||||
value = value.slice(0, item['text-input'].max_length)
|
||||
|
||||
return {
|
||||
...item['text-input'],
|
||||
default: value || item.default || item['text-input'].default,
|
||||
@ -334,11 +278,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}
|
||||
})
|
||||
}, [initInputs, appParams])
|
||||
|
||||
const allInputsHidden = useMemo(() => {
|
||||
return inputsForms.length > 0 && inputsForms.every(item => item.hide === true)
|
||||
}, [inputsForms])
|
||||
|
||||
useEffect(() => {
|
||||
// init inputs from url params
|
||||
(async () => {
|
||||
@ -348,16 +290,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
setInitUserVariables(userVariables)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const conversationInputs: Record<string, any> = {}
|
||||
|
||||
inputsForms.forEach((item: any) => {
|
||||
conversationInputs[item.variable] = item.default || null
|
||||
})
|
||||
handleNewConversationInputsChange(conversationInputs)
|
||||
}, [handleNewConversationInputsChange, inputsForms])
|
||||
|
||||
const { data: newConversation } = useShareConversationName({
|
||||
conversationId: newConversationId,
|
||||
appSourceType,
|
||||
@ -373,7 +312,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [appConversationData, appConversationDataLoading])
|
||||
const conversationList = useMemo(() => {
|
||||
const data = originConversationList.slice()
|
||||
|
||||
if (showNewConversationItemInList && data[0]?.id !== '') {
|
||||
data.unshift({
|
||||
id: '',
|
||||
@ -384,12 +322,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}
|
||||
return data
|
||||
}, [originConversationList, showNewConversationItemInList, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (newConversation) {
|
||||
setOriginConversationList(produce((draft) => {
|
||||
const index = draft.findIndex(item => item.id === newConversation.id)
|
||||
|
||||
if (index > -1)
|
||||
draft[index] = newConversation
|
||||
else
|
||||
@ -397,16 +333,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}))
|
||||
}
|
||||
}, [newConversation])
|
||||
|
||||
const currentConversationItem = useMemo(() => {
|
||||
let conversationItem = conversationList.find(item => item.id === currentConversationId)
|
||||
|
||||
if (!conversationItem && pinnedConversationList.length)
|
||||
conversationItem = pinnedConversationList.find(item => item.id === currentConversationId)
|
||||
|
||||
return conversationItem
|
||||
}, [conversationList, currentConversationId, pinnedConversationList])
|
||||
|
||||
const currentConversationLatestInputs = useMemo(() => {
|
||||
if (!currentConversationId || !appChatListData?.data.length)
|
||||
return newConversationInputsRef.current || {}
|
||||
@ -417,12 +349,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
if (currentConversationItem)
|
||||
setCurrentConversationInputs(currentConversationLatestInputs || {})
|
||||
}, [currentConversationItem, currentConversationLatestInputs])
|
||||
|
||||
const { notify } = useToastContext()
|
||||
const checkInputsRequired = useCallback((silent?: boolean) => {
|
||||
if (allInputsHidden)
|
||||
return true
|
||||
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
|
||||
@ -430,13 +359,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (fileIsUploading)
|
||||
return
|
||||
|
||||
if (!newConversationInputsRef.current[variable] && !silent)
|
||||
hasEmptyInput = label as string
|
||||
|
||||
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) {
|
||||
const files = newConversationInputsRef.current[variable]
|
||||
if (Array.isArray(files))
|
||||
@ -446,26 +372,25 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEmptyInput) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
if (fileIsUploading) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
}, [inputsForms, notify, t, allInputsHidden])
|
||||
}, [inputsForms, t, allInputsHidden])
|
||||
const handleStartChat = useCallback((callback: any) => {
|
||||
if (checkInputsRequired()) {
|
||||
setShowNewConversationItemInList(true)
|
||||
callback?.()
|
||||
}
|
||||
}, [setShowNewConversationItemInList, checkInputsRequired])
|
||||
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: noop })
|
||||
const currentChatInstanceRef = useRef<{
|
||||
handleStop: () => void
|
||||
}>({ handleStop: noop })
|
||||
const handleChangeConversation = useCallback((conversationId: string) => {
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setNewConversationId('')
|
||||
@ -487,76 +412,48 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const handleUpdateConversationList = useCallback(() => {
|
||||
invalidateShareConversations()
|
||||
}, [invalidateShareConversations])
|
||||
|
||||
const handlePinConversation = useCallback(async (conversationId: string) => {
|
||||
await pinConversation(appSourceType, appId, conversationId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
toast.success(t('api.success', { ns: 'common' }))
|
||||
handleUpdateConversationList()
|
||||
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||
|
||||
}, [appSourceType, appId, t, handleUpdateConversationList])
|
||||
const handleUnpinConversation = useCallback(async (conversationId: string) => {
|
||||
await unpinConversation(appSourceType, appId, conversationId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
toast.success(t('api.success', { ns: 'common' }))
|
||||
handleUpdateConversationList()
|
||||
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||
|
||||
}, [appSourceType, appId, t, handleUpdateConversationList])
|
||||
const [conversationDeleting, setConversationDeleting] = useState(false)
|
||||
const handleDeleteConversation = useCallback(async (
|
||||
conversationId: string,
|
||||
{
|
||||
onSuccess,
|
||||
}: Callback,
|
||||
) => {
|
||||
const handleDeleteConversation = useCallback(async (conversationId: string, { onSuccess }: Callback) => {
|
||||
if (conversationDeleting)
|
||||
return
|
||||
|
||||
try {
|
||||
setConversationDeleting(true)
|
||||
await delConversation(appSourceType, appId, conversationId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
toast.success(t('api.success', { ns: 'common' }))
|
||||
onSuccess()
|
||||
}
|
||||
finally {
|
||||
setConversationDeleting(false)
|
||||
}
|
||||
|
||||
if (conversationId === currentConversationId)
|
||||
handleNewConversation()
|
||||
|
||||
handleUpdateConversationList()
|
||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting])
|
||||
|
||||
}, [isInstalledApp, appId, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting])
|
||||
const [conversationRenaming, setConversationRenaming] = useState(false)
|
||||
const handleRenameConversation = useCallback(async (
|
||||
conversationId: string,
|
||||
newName: string,
|
||||
{
|
||||
onSuccess,
|
||||
}: Callback,
|
||||
) => {
|
||||
const handleRenameConversation = useCallback(async (conversationId: string, newName: string, { onSuccess }: Callback) => {
|
||||
if (conversationRenaming)
|
||||
return
|
||||
|
||||
if (!newName.trim()) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('chat.conversationNameCanNotEmpty', { ns: 'common' }),
|
||||
})
|
||||
toast.error(t('chat.conversationNameCanNotEmpty', { ns: 'common' }))
|
||||
return
|
||||
}
|
||||
|
||||
setConversationRenaming(true)
|
||||
try {
|
||||
await renameConversation(appSourceType, appId, conversationId, newName)
|
||||
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }),
|
||||
})
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
setOriginConversationList(produce((draft) => {
|
||||
const index = originConversationList.findIndex(item => item.id === conversationId)
|
||||
const item = draft[index]
|
||||
|
||||
draft[index] = {
|
||||
...item,
|
||||
name: newName,
|
||||
@ -567,20 +464,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
finally {
|
||||
setConversationRenaming(false)
|
||||
}
|
||||
}, [isInstalledApp, appId, notify, t, conversationRenaming, originConversationList])
|
||||
|
||||
}, [isInstalledApp, appId, t, conversationRenaming, originConversationList])
|
||||
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
||||
setNewConversationId(newConversationId)
|
||||
handleConversationIdInfoChange(newConversationId)
|
||||
setShowNewConversationItemInList(false)
|
||||
invalidateShareConversations()
|
||||
}, [handleConversationIdInfoChange, invalidateShareConversations])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
}, [appSourceType, appId, t, notify])
|
||||
|
||||
toast.success(t('api.success', { ns: 'common' }))
|
||||
}, [appSourceType, appId, t])
|
||||
return {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
|
||||
@ -5,8 +5,8 @@ import { TransferMethod } from '@/types/app'
|
||||
import { useCheckInputsForms } from '../check-input-forms-hooks'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
describe('useCheckInputsForms', () => {
|
||||
|
||||
@ -20,8 +20,8 @@ vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
|
||||
@ -5,7 +5,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import Toast from '../../../toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ThemeBuilder } from '../../embedded-chatbot/theme/theme-context'
|
||||
import { ChatContextProvider } from '../context-provider'
|
||||
import Question from '../question'
|
||||
@ -179,7 +179,7 @@ describe('Question component', () => {
|
||||
|
||||
it('should call copy-to-clipboard and show a toast when copy action is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const toastSpy = vi.spyOn(Toast, 'notify')
|
||||
const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
|
||||
|
||||
renderWithProvider(makeItem())
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ const {
|
||||
|
||||
vi.mock('copy-to-clipboard', () => ({ default: vi.fn() }))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../../types'
|
||||
import type { ChatItem, Feedback } from '../../types'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
@ -17,8 +10,8 @@ import AnnotationCtrlButton from '@/app/components/base/features/new-feature-pan
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useChatContext } from '../context'
|
||||
|
||||
@ -32,14 +25,11 @@ type OperationProps = {
|
||||
hasWorkflowProcess: boolean
|
||||
noChatInput?: boolean
|
||||
}
|
||||
|
||||
const stringifyCopyValue = (value: unknown) => {
|
||||
if (typeof value === 'string')
|
||||
return value
|
||||
|
||||
if (value === null || typeof value === 'undefined')
|
||||
return ''
|
||||
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
@ -47,196 +37,132 @@ const stringifyCopyValue = (value: unknown) => {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
const buildCopyContentFromLLMGenerationItems = (llmGenerationItems?: ChatItem['llmGenerationItems']) => {
|
||||
if (!llmGenerationItems?.length)
|
||||
return ''
|
||||
|
||||
const hasStructuredItems = llmGenerationItems.some(item => item.type !== 'text')
|
||||
if (!hasStructuredItems)
|
||||
return ''
|
||||
|
||||
return llmGenerationItems
|
||||
.map((item) => {
|
||||
if (item.type === 'text')
|
||||
return item.text || ''
|
||||
|
||||
if (item.type === 'thought')
|
||||
return item.thoughtOutput ? `[THOUGHT]\n${item.thoughtOutput}` : ''
|
||||
|
||||
if (item.type === 'tool') {
|
||||
const sections = [
|
||||
`[TOOL] ${item.toolName || ''}`.trim(),
|
||||
]
|
||||
|
||||
if (item.toolArguments)
|
||||
sections.push(`INPUT:\n${stringifyCopyValue(item.toolArguments)}`)
|
||||
if (typeof item.toolOutput !== 'undefined')
|
||||
sections.push(`OUTPUT:\n${stringifyCopyValue(item.toolOutput)}`)
|
||||
if (item.toolError)
|
||||
sections.push(`ERROR:\n${item.toolError}`)
|
||||
|
||||
return sections.join('\n')
|
||||
}
|
||||
|
||||
if (item.type === 'model') {
|
||||
const sections = [
|
||||
`[MODEL] ${item.modelName || ''}`.trim(),
|
||||
]
|
||||
|
||||
if (typeof item.modelOutput !== 'undefined')
|
||||
sections.push(`OUTPUT:\n${stringifyCopyValue(item.modelOutput)}`)
|
||||
|
||||
return sections.join('\n')
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
const buildCopyContentFromAgentThoughts = (agentThoughts?: ChatItem['agent_thoughts']) => {
|
||||
if (!agentThoughts?.length)
|
||||
return ''
|
||||
|
||||
return agentThoughts
|
||||
.map((thought) => {
|
||||
const sections = [
|
||||
`[AGENT] ${thought.tool || ''}`.trim(),
|
||||
]
|
||||
|
||||
if (thought.thought)
|
||||
sections.push(`THOUGHT:\n${thought.thought}`)
|
||||
if (thought.tool_input)
|
||||
sections.push(`INPUT:\n${thought.tool_input}`)
|
||||
if (thought.observation)
|
||||
sections.push(`OUTPUT:\n${thought.observation}`)
|
||||
|
||||
return sections.join('\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
showPromptLog,
|
||||
maxSize,
|
||||
contentWidth,
|
||||
hasWorkflowProcess,
|
||||
noChatInput,
|
||||
}) => {
|
||||
const Operation: FC<OperationProps> = ({ item, question, index, showPromptLog, maxSize, contentWidth, hasWorkflowProcess, noChatInput }) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
config,
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
onFeedback,
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
const { config, onAnnotationAdded, onAnnotationEdited, onAnnotationRemoved, onFeedback, onRegenerate } = useChatContext()
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const [isShowFeedbackModal, setIsShowFeedbackModal] = useState(false)
|
||||
const [feedbackContent, setFeedbackContent] = useState('')
|
||||
const {
|
||||
id,
|
||||
isOpeningStatement,
|
||||
content: messageContent,
|
||||
annotation,
|
||||
feedback,
|
||||
adminFeedback,
|
||||
agent_thoughts,
|
||||
humanInputFormDataList,
|
||||
} = item
|
||||
const { id, isOpeningStatement, content: messageContent, annotation, feedback, adminFeedback, agent_thoughts, humanInputFormDataList } = item
|
||||
const [userLocalFeedback, setUserLocalFeedback] = useState(feedback)
|
||||
const [adminLocalFeedback, setAdminLocalFeedback] = useState(adminFeedback)
|
||||
const [feedbackTarget, setFeedbackTarget] = useState<'user' | 'admin'>('user')
|
||||
|
||||
// Separate feedback types for display
|
||||
const userFeedback = feedback
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (agent_thoughts?.length)
|
||||
return agent_thoughts.reduce((acc, cur) => acc + cur.thought, '')
|
||||
|
||||
return messageContent
|
||||
}, [agent_thoughts, messageContent])
|
||||
|
||||
const copyContent = useMemo(() => {
|
||||
const llmGenerationCopyContent = buildCopyContentFromLLMGenerationItems(item.llmGenerationItems)
|
||||
if (llmGenerationCopyContent)
|
||||
return llmGenerationCopyContent
|
||||
|
||||
const agentThoughtCopyContent = buildCopyContentFromAgentThoughts(agent_thoughts)
|
||||
if (agentThoughtCopyContent)
|
||||
return agentThoughtCopyContent
|
||||
|
||||
return messageContent
|
||||
}, [item.llmGenerationItems, agent_thoughts, messageContent])
|
||||
|
||||
const displayUserFeedback = userLocalFeedback ?? userFeedback
|
||||
|
||||
const hasUserFeedback = !!displayUserFeedback?.rating
|
||||
const hasAdminFeedback = !!adminLocalFeedback?.rating
|
||||
|
||||
const shouldShowUserFeedbackBar = !isOpeningStatement && config?.supportFeedback && !!onFeedback && !config?.supportAnnotation
|
||||
const shouldShowAdminFeedbackBar = !isOpeningStatement && config?.supportFeedback && !!onFeedback && !!config?.supportAnnotation
|
||||
|
||||
const userFeedbackLabel = t('table.header.userRate', { ns: 'appLog' }) || 'User feedback'
|
||||
const adminFeedbackLabel = t('table.header.adminRate', { ns: 'appLog' }) || 'Admin feedback'
|
||||
const feedbackTooltipClassName = 'max-w-[260px]'
|
||||
|
||||
const buildFeedbackTooltip = (feedbackData?: Feedback | null, label = userFeedbackLabel) => {
|
||||
if (!feedbackData?.rating)
|
||||
return label
|
||||
|
||||
const ratingLabel = feedbackData.rating === 'like'
|
||||
? (t('detail.operation.like', { ns: 'appLog' }) || 'like')
|
||||
: (t('detail.operation.dislike', { ns: 'appLog' }) || 'dislike')
|
||||
const feedbackText = feedbackData.content?.trim()
|
||||
|
||||
if (feedbackText)
|
||||
return `${label}: ${ratingLabel} - ${feedbackText}`
|
||||
|
||||
return `${label}: ${ratingLabel}`
|
||||
}
|
||||
|
||||
const handleFeedback = async (rating: 'like' | 'dislike' | null, content?: string, target: 'user' | 'admin' = 'user') => {
|
||||
if (!config?.supportFeedback || !onFeedback)
|
||||
return
|
||||
|
||||
await onFeedback?.(id, { rating, content })
|
||||
|
||||
const nextFeedback = rating === null ? { rating: null } : { rating, content }
|
||||
|
||||
if (target === 'admin')
|
||||
setAdminLocalFeedback(nextFeedback)
|
||||
else
|
||||
setUserLocalFeedback(nextFeedback)
|
||||
}
|
||||
|
||||
const handleLikeClick = (target: 'user' | 'admin') => {
|
||||
handleFeedback('like', undefined, target)
|
||||
}
|
||||
|
||||
const handleDislikeClick = (target: 'user' | 'admin') => {
|
||||
setFeedbackTarget(target)
|
||||
setIsShowFeedbackModal(true)
|
||||
}
|
||||
|
||||
const handleFeedbackSubmit = async () => {
|
||||
await handleFeedback('dislike', feedbackContent, feedbackTarget)
|
||||
setFeedbackContent('')
|
||||
setIsShowFeedbackModal(false)
|
||||
}
|
||||
|
||||
const handleFeedbackCancel = () => {
|
||||
setFeedbackContent('')
|
||||
setIsShowFeedbackModal(false)
|
||||
}
|
||||
|
||||
const operationWidth = useMemo(() => {
|
||||
let width = 0
|
||||
if (!isOpeningStatement)
|
||||
@ -251,40 +177,18 @@ const Operation: FC<OperationProps> = ({
|
||||
width += hasUserFeedback ? 28 + 8 : 60 + 8
|
||||
if (shouldShowAdminFeedbackBar)
|
||||
width += (hasAdminFeedback ? 28 : 60) + 8 + (hasUserFeedback ? 28 : 0)
|
||||
|
||||
return width
|
||||
}, [config?.annotation_reply?.enabled, config?.supportAnnotation, config?.text_to_speech?.enabled, hasAdminFeedback, hasUserFeedback, isOpeningStatement, shouldShowAdminFeedbackBar, shouldShowUserFeedbackBar, showPromptLog])
|
||||
|
||||
const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute flex justify-end gap-1',
|
||||
hasWorkflowProcess && '-bottom-4 right-2',
|
||||
!positionRight && '-bottom-4 right-2',
|
||||
!hasWorkflowProcess && positionRight && '!top-[9px]',
|
||||
)}
|
||||
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
|
||||
data-testid="operation-bar"
|
||||
>
|
||||
<div className={cn('absolute flex justify-end gap-1', hasWorkflowProcess && '-bottom-4 right-2', !positionRight && '-bottom-4 right-2', !hasWorkflowProcess && positionRight && '!top-[9px]')} style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}} data-testid="operation-bar">
|
||||
{shouldShowUserFeedbackBar && !humanInputFormDataList?.length && (
|
||||
<div className={cn(
|
||||
'ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm',
|
||||
hasUserFeedback ? 'flex' : 'hidden group-hover:flex',
|
||||
)}
|
||||
>
|
||||
<div className={cn('ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm', hasUserFeedback ? 'flex' : 'hidden group-hover:flex')}>
|
||||
{hasUserFeedback
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(displayUserFeedback, userFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={displayUserFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Destructive}
|
||||
onClick={() => handleFeedback(null, undefined, 'user')}
|
||||
>
|
||||
<Tooltip popupContent={buildFeedbackTooltip(displayUserFeedback, userFeedbackLabel)} popupClassName={feedbackTooltipClassName}>
|
||||
<ActionButton state={displayUserFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Destructive} onClick={() => handleFeedback(null, undefined, 'user')}>
|
||||
{displayUserFeedback?.rating === 'like'
|
||||
? <div className="i-ri-thumb-up-line h-4 w-4" />
|
||||
: <div className="i-ri-thumb-down-line h-4 w-4" />}
|
||||
@ -293,16 +197,10 @@ const Operation: FC<OperationProps> = ({
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<ActionButton
|
||||
state={displayUserFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Default}
|
||||
onClick={() => handleLikeClick('user')}
|
||||
>
|
||||
<ActionButton state={displayUserFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Default} onClick={() => handleLikeClick('user')}>
|
||||
<div className="i-ri-thumb-up-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
state={displayUserFeedback?.rating === 'dislike' ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onClick={() => handleDislikeClick('user')}
|
||||
>
|
||||
<ActionButton state={displayUserFeedback?.rating === 'dislike' ? ActionButtonState.Destructive : ActionButtonState.Default} onClick={() => handleDislikeClick('user')}>
|
||||
<div className="i-ri-thumb-down-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</>
|
||||
@ -310,17 +208,10 @@ const Operation: FC<OperationProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{shouldShowAdminFeedbackBar && !humanInputFormDataList?.length && (
|
||||
<div className={cn(
|
||||
'ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm',
|
||||
(hasAdminFeedback || hasUserFeedback) ? 'flex' : 'hidden group-hover:flex',
|
||||
)}
|
||||
>
|
||||
<div className={cn('ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm', (hasAdminFeedback || hasUserFeedback) ? 'flex' : 'hidden group-hover:flex')}>
|
||||
{/* User Feedback Display */}
|
||||
{displayUserFeedback?.rating && (
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(displayUserFeedback, userFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<Tooltip popupContent={buildFeedbackTooltip(displayUserFeedback, userFeedbackLabel)} popupClassName={feedbackTooltipClassName}>
|
||||
{displayUserFeedback.rating === 'like'
|
||||
? (
|
||||
<ActionButton state={ActionButtonState.Active}>
|
||||
@ -339,14 +230,8 @@ const Operation: FC<OperationProps> = ({
|
||||
{displayUserFeedback?.rating && <div className="mx-1 h-3 w-[0.5px] bg-components-actionbar-border" />}
|
||||
{hasAdminFeedback
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={adminLocalFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Destructive}
|
||||
onClick={() => handleFeedback(null, undefined, 'admin')}
|
||||
>
|
||||
<Tooltip popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)} popupClassName={feedbackTooltipClassName}>
|
||||
<ActionButton state={adminLocalFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Destructive} onClick={() => handleFeedback(null, undefined, 'admin')}>
|
||||
{adminLocalFeedback?.rating === 'like'
|
||||
? <div className="i-ri-thumb-up-line h-4 w-4" />
|
||||
: <div className="i-ri-thumb-down-line h-4 w-4" />}
|
||||
@ -355,25 +240,13 @@ const Operation: FC<OperationProps> = ({
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={adminLocalFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Default}
|
||||
onClick={() => handleLikeClick('admin')}
|
||||
>
|
||||
<Tooltip popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)} popupClassName={feedbackTooltipClassName}>
|
||||
<ActionButton state={adminLocalFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Default} onClick={() => handleLikeClick('admin')}>
|
||||
<div className="i-ri-thumb-up-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={adminLocalFeedback?.rating === 'dislike' ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onClick={() => handleDislikeClick('admin')}
|
||||
>
|
||||
<Tooltip popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)} popupClassName={feedbackTooltipClassName}>
|
||||
<ActionButton state={adminLocalFeedback?.rating === 'dislike' ? ActionButtonState.Destructive : ActionButtonState.Default} onClick={() => handleDislikeClick('admin')}>
|
||||
<div className="i-ri-thumb-down-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@ -388,18 +261,12 @@ const Operation: FC<OperationProps> = ({
|
||||
)}
|
||||
{!isOpeningStatement && (
|
||||
<div className="ml-1 hidden items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex" data-testid="operation-actions">
|
||||
{(config?.text_to_speech?.enabled && !humanInputFormDataList?.length) && (
|
||||
<NewAudioButton
|
||||
id={id}
|
||||
value={content}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
/>
|
||||
)}
|
||||
{(config?.text_to_speech?.enabled && !humanInputFormDataList?.length) && (<NewAudioButton id={id} value={content} voice={config?.text_to_speech?.voice} />)}
|
||||
{!humanInputFormDataList?.length && (
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
copy(copyContent)
|
||||
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
data-testid="copy-btn"
|
||||
>
|
||||
@ -411,55 +278,19 @@ const Operation: FC<OperationProps> = ({
|
||||
<div className="i-ri-reset-left-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && (
|
||||
<AnnotationCtrlButton
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
cached={!!annotation?.id}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
/>
|
||||
)}
|
||||
{config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && (<AnnotationCtrlButton appId={config?.appId || ''} messageId={id} cached={!!annotation?.id} query={question} answer={content} onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)} onEdit={() => setIsShowReplyModal(true)} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)}
|
||||
onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)}
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
<EditReplyModal isShow={isShowReplyModal} onHide={() => setIsShowReplyModal(false)} query={question} answer={content} onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)} onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)} appId={config?.appId || ''} messageId={id} annotationId={annotation?.id || ''} createdAt={annotation?.created_at} onRemove={() => onAnnotationRemoved?.(index)} />
|
||||
{isShowFeedbackModal && (
|
||||
<Modal
|
||||
title={t('feedback.title', { ns: 'common' }) || 'Provide Feedback'}
|
||||
subTitle={t('feedback.subtitle', { ns: 'common' }) || 'Please tell us what went wrong with this response'}
|
||||
onClose={handleFeedbackCancel}
|
||||
onConfirm={handleFeedbackSubmit}
|
||||
onCancel={handleFeedbackCancel}
|
||||
confirmButtonText={t('operation.submit', { ns: 'common' }) || 'Submit'}
|
||||
cancelButtonText={t('operation.cancel', { ns: 'common' }) || 'Cancel'}
|
||||
>
|
||||
<Modal title={t('feedback.title', { ns: 'common' }) || 'Provide Feedback'} subTitle={t('feedback.subtitle', { ns: 'common' }) || 'Please tell us what went wrong with this response'} onClose={handleFeedbackCancel} onConfirm={handleFeedbackSubmit} onCancel={handleFeedbackCancel} confirmButtonText={t('operation.submit', { ns: 'common' }) || 'Submit'} cancelButtonText={t('operation.cancel', { ns: 'common' }) || 'Cancel'}>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="mb-2 block text-text-secondary system-sm-semibold">
|
||||
{t('feedback.content', { ns: 'common' }) || 'Feedback Content'}
|
||||
</label>
|
||||
<Textarea
|
||||
value={feedbackContent}
|
||||
onChange={e => setFeedbackContent(e.target.value)}
|
||||
placeholder={t('feedback.placeholder', { ns: 'common' }) || 'Please describe what went wrong or how we can improve...'}
|
||||
rows={4}
|
||||
className="w-full"
|
||||
/>
|
||||
<Textarea value={feedbackContent} onChange={e => setFeedbackContent(e.target.value)} placeholder={t('feedback.placeholder', { ns: 'common' }) || 'Please describe what went wrong or how we can improve...'} rows={4} className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@ -467,5 +298,4 @@ const Operation: FC<OperationProps> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Operation)
|
||||
|
||||
@ -175,8 +175,8 @@ vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
// ---------------------------------------------------------------------------
|
||||
// Toast context
|
||||
// ---------------------------------------------------------------------------
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify, close: vi.fn() }),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -1,28 +1,18 @@
|
||||
import type { Theme } from '../../embedded-chatbot/theme/theme-context'
|
||||
import type {
|
||||
EnableType,
|
||||
OnSend,
|
||||
} from '../../types'
|
||||
import type { EnableType, OnSend } from '../../types'
|
||||
import type { InputForm } from '../type'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { decode } from 'html-entities'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
||||
import { FileListInChatInput } from '@/app/components/base/file-uploader'
|
||||
import { useFile } from '@/app/components/base/file-uploader/hooks'
|
||||
import {
|
||||
FileContextProvider,
|
||||
useFileStore,
|
||||
} from '@/app/components/base/file-uploader/store'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { FileContextProvider, useFileStore } from '@/app/components/base/file-uploader/store'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import VoiceInput from '@/app/components/base/voice-input'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -53,71 +43,34 @@ type ChatInputAreaProps = {
|
||||
*/
|
||||
sendOnEnter?: boolean
|
||||
}
|
||||
const ChatInputArea = ({
|
||||
readonly,
|
||||
botName,
|
||||
showFeatureBar,
|
||||
showFileUpload,
|
||||
featureBarDisabled,
|
||||
onFeatureBarClick,
|
||||
visionConfig,
|
||||
speechToTextConfig = { enabled: true },
|
||||
onSend,
|
||||
inputs = {},
|
||||
inputsForm = [],
|
||||
theme,
|
||||
isResponding,
|
||||
disabled,
|
||||
sendOnEnter = true,
|
||||
}: ChatInputAreaProps) => {
|
||||
const ChatInputArea = ({ readonly, botName, showFeatureBar, showFileUpload, featureBarDisabled, onFeatureBarClick, visionConfig, speechToTextConfig = { enabled: true }, onSend, inputs = {}, inputsForm = [], theme, isResponding, disabled, sendOnEnter = true }: ChatInputAreaProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const {
|
||||
wrapperRef,
|
||||
textareaRef,
|
||||
textValueRef,
|
||||
holdSpaceRef,
|
||||
handleTextareaResize,
|
||||
isMultipleLine,
|
||||
} = useTextAreaHeight()
|
||||
const { wrapperRef, textareaRef, textValueRef, holdSpaceRef, handleTextareaResize, isMultipleLine } = useTextAreaHeight()
|
||||
const [query, setQuery] = useState('')
|
||||
const [showVoiceInput, setShowVoiceInput] = useState(false)
|
||||
const filesStore = useFileStore()
|
||||
const {
|
||||
handleDragFileEnter,
|
||||
handleDragFileLeave,
|
||||
handleDragFileOver,
|
||||
handleDropFile,
|
||||
handleClipboardPasteFile,
|
||||
isDragActive,
|
||||
} = useFile(visionConfig!, false)
|
||||
const { handleDragFileEnter, handleDragFileLeave, handleDragFileOver, handleDropFile, handleClipboardPasteFile, isDragActive } = useFile(visionConfig!, false)
|
||||
const { checkInputsForm } = useCheckInputsForms()
|
||||
const historyRef = useRef([''])
|
||||
const [currentIndex, setCurrentIndex] = useState(-1)
|
||||
const isComposingRef = useRef(false)
|
||||
|
||||
const handleQueryChange = useCallback(
|
||||
(value: string) => {
|
||||
setQuery(value)
|
||||
setTimeout(handleTextareaResize, 0)
|
||||
},
|
||||
[handleTextareaResize],
|
||||
)
|
||||
|
||||
const handleQueryChange = useCallback((value: string) => {
|
||||
setQuery(value)
|
||||
setTimeout(handleTextareaResize, 0)
|
||||
}, [handleTextareaResize])
|
||||
const handleSend = () => {
|
||||
if (isResponding) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (onSend) {
|
||||
const { files, setFiles } = filesStore.getState()
|
||||
if (files.some(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
if (!query || !query.trim()) {
|
||||
notify({ type: 'info', message: t('errorMessage.queryRequired', { ns: 'appAnnotation' }) })
|
||||
toast.info(t('errorMessage.queryRequired', { ns: 'appAnnotation' }))
|
||||
return
|
||||
}
|
||||
if (checkInputsForm(inputs, inputsForm)) {
|
||||
@ -145,7 +98,6 @@ const ChatInputArea = ({
|
||||
const isSendCombo = sendOnEnter
|
||||
? (e.key === 'Enter' && !e.shiftKey)
|
||||
: (e.key === 'Enter' && e.shiftKey)
|
||||
|
||||
if (isSendCombo && !e.nativeEvent.isComposing) {
|
||||
// if isComposing, exit
|
||||
if (isComposingRef.current)
|
||||
@ -176,101 +128,36 @@ const ChatInputArea = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowVoiceInput = useCallback(() => {
|
||||
(Recorder as any).getPermission().then(() => {
|
||||
setShowVoiceInput(true)
|
||||
}, () => {
|
||||
notify({ type: 'error', message: t('voiceInput.notAllow', { ns: 'common' }) })
|
||||
toast.error(t('voiceInput.notAllow', { ns: 'common' }))
|
||||
})
|
||||
}, [t, notify])
|
||||
|
||||
const operation = (
|
||||
<Operation
|
||||
ref={holdSpaceRef}
|
||||
readonly={readonly}
|
||||
fileConfig={visionConfig}
|
||||
speechToTextConfig={speechToTextConfig}
|
||||
onShowVoiceInput={handleShowVoiceInput}
|
||||
onSend={handleSend}
|
||||
theme={theme}
|
||||
/>
|
||||
)
|
||||
|
||||
}, [t])
|
||||
const operation = (<Operation ref={holdSpaceRef} readonly={readonly} fileConfig={visionConfig} speechToTextConfig={speechToTextConfig} onShowVoiceInput={handleShowVoiceInput} onSend={handleSend} theme={theme} />)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 overflow-hidden rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[9px] shadow-md',
|
||||
isDragActive && 'border border-dashed border-components-option-card-option-selected-border',
|
||||
disabled && 'pointer-events-none border-components-panel-border opacity-50 shadow-none',
|
||||
)}
|
||||
>
|
||||
<div className={cn('relative z-10 overflow-hidden rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[9px] shadow-md', isDragActive && 'border border-dashed border-components-option-card-option-selected-border', disabled && 'pointer-events-none border-components-panel-border opacity-50 shadow-none')}>
|
||||
<div className="relative max-h-[158px] overflow-y-auto overflow-x-hidden px-[9px] pt-[9px]">
|
||||
<FileListInChatInput fileConfig={visionConfig!} />
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div ref={wrapperRef} className="flex items-center justify-between">
|
||||
<div className="relative flex w-full grow items-center">
|
||||
<div
|
||||
ref={textValueRef}
|
||||
className="pointer-events-none invisible absolute h-auto w-auto whitespace-pre p-1 leading-6 body-lg-regular"
|
||||
>
|
||||
<div ref={textValueRef} className="pointer-events-none invisible absolute h-auto w-auto whitespace-pre p-1 leading-6 body-lg-regular">
|
||||
{query}
|
||||
</div>
|
||||
<Textarea
|
||||
ref={ref => textareaRef.current = ref as any}
|
||||
className={cn(
|
||||
'w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none body-lg-regular',
|
||||
)}
|
||||
placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
value={query}
|
||||
onChange={e => handleQueryChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
onPaste={handleClipboardPasteFile}
|
||||
onDragEnter={handleDragFileEnter}
|
||||
onDragLeave={handleDragFileLeave}
|
||||
onDragOver={handleDragFileOver}
|
||||
onDrop={handleDropFile}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none body-lg-regular')} placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
|
||||
</div>
|
||||
{
|
||||
!isMultipleLine && operation
|
||||
}
|
||||
{!isMultipleLine && operation}
|
||||
</div>
|
||||
{
|
||||
showVoiceInput && (
|
||||
<VoiceInput
|
||||
onCancel={() => setShowVoiceInput(false)}
|
||||
onConverted={text => handleQueryChange(text)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showVoiceInput && (<VoiceInput onCancel={() => setShowVoiceInput(false)} onConverted={text => handleQueryChange(text)} />)}
|
||||
</div>
|
||||
{
|
||||
isMultipleLine && (
|
||||
<div className="px-[9px]">{operation}</div>
|
||||
)
|
||||
}
|
||||
{isMultipleLine && (<div className="px-[9px]">{operation}</div>)}
|
||||
</div>
|
||||
{showFeatureBar && (
|
||||
<FeatureBar
|
||||
showFileUpload={showFileUpload}
|
||||
disabled={featureBarDisabled}
|
||||
onFeatureBarClick={readonly ? noop : onFeatureBarClick}
|
||||
hideEditEntrance={readonly}
|
||||
/>
|
||||
)}
|
||||
{showFeatureBar && (<FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={readonly ? noop : onFeatureBarClick} hideEditEntrance={readonly} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ChatInputAreaWrapper = (props: ChatInputAreaProps) => {
|
||||
return (
|
||||
<FileContextProvider>
|
||||
@ -278,5 +165,4 @@ const ChatInputAreaWrapper = (props: ChatInputAreaProps) => {
|
||||
</FileContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInputAreaWrapper
|
||||
|
||||
@ -1,30 +1,24 @@
|
||||
import type { InputForm } from './type'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
export const useCheckInputsForms = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const checkInputsForm = useCallback((inputs: Record<string, any>, inputsForm: InputForm[]) => {
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForm.filter(({ required, type }) => required && type !== InputVarType.checkbox) // boolean can be not checked
|
||||
|
||||
if (requiredVars?.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (fileIsUploading)
|
||||
return
|
||||
|
||||
if (!inputs[variable])
|
||||
hasEmptyInput = label as string
|
||||
|
||||
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputs[variable]) {
|
||||
const files = inputs[variable]
|
||||
if (Array.isArray(files))
|
||||
@ -34,20 +28,16 @@ export const useCheckInputsForms = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEmptyInput) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
if (fileIsUploading) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
}, [notify, t])
|
||||
|
||||
}, [t])
|
||||
return {
|
||||
checkInputsForm,
|
||||
}
|
||||
|
||||
@ -24,10 +24,8 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: vi.fn(),
|
||||
}),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
|
||||
|
||||
@ -29,7 +29,7 @@ import {
|
||||
getProcessedFiles,
|
||||
getProcessedFilesFromResponse,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { useParams, usePathname } from '@/next/navigation'
|
||||
@ -65,7 +65,6 @@ export const useChat = (
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTime } = useTimestamp()
|
||||
const { notify } = useToastContext()
|
||||
const conversationIdRef = useRef('')
|
||||
const hasStopRespondedRef = useRef(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
@ -637,7 +636,7 @@ export const useChat = (
|
||||
setSuggestedQuestions([])
|
||||
|
||||
if (isRespondingRef.current) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1269,7 +1268,6 @@ export const useChat = (
|
||||
config?.suggested_questions_after_answer,
|
||||
updateCurrentQAOnTree,
|
||||
updateChatTreeNode,
|
||||
notify,
|
||||
handleResponding,
|
||||
formatTime,
|
||||
createAudioPlayerManager,
|
||||
|
||||
@ -1,25 +1,16 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { Theme } from '../embedded-chatbot/theme/theme-context'
|
||||
import type { ChatItem } from '../types'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ActionButton from '../../action-button'
|
||||
import Button from '../../button'
|
||||
import Toast from '../../toast'
|
||||
import { CssTransform } from '../embedded-chatbot/theme/utils'
|
||||
import ContentSwitch from './content-switch'
|
||||
import { useChatContext } from './context'
|
||||
@ -32,38 +23,20 @@ type QuestionProps = {
|
||||
switchSibling?: (siblingMessageId: string) => void
|
||||
hideAvatar?: boolean
|
||||
}
|
||||
|
||||
const Question: FC<QuestionProps> = ({
|
||||
item,
|
||||
questionIcon,
|
||||
theme,
|
||||
enableEdit = true,
|
||||
switchSibling,
|
||||
hideAvatar,
|
||||
}) => {
|
||||
const Question: FC<QuestionProps> = ({ item, questionIcon, theme, enableEdit = true, switchSibling, hideAvatar }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
content,
|
||||
message_files,
|
||||
} = item
|
||||
|
||||
const {
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
|
||||
const { content, message_files } = item
|
||||
const { onRegenerate } = useChatContext()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedContent, setEditedContent] = useState(content)
|
||||
const [contentWidth, setContentWidth] = useState(0)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const isComposingRef = useRef(false)
|
||||
const compositionEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setIsEditing(true)
|
||||
setEditedContent(content)
|
||||
}, [content])
|
||||
|
||||
const handleResend = useCallback(() => {
|
||||
if (compositionEndTimerRef.current) {
|
||||
clearTimeout(compositionEndTimerRef.current)
|
||||
@ -73,7 +46,6 @@ const Question: FC<QuestionProps> = ({
|
||||
setIsEditing(false)
|
||||
onRegenerate?.(item, { message: editedContent, files: message_files })
|
||||
}, [editedContent, message_files, item, onRegenerate])
|
||||
|
||||
const handleCancelEditing = useCallback(() => {
|
||||
if (compositionEndTimerRef.current) {
|
||||
clearTimeout(compositionEndTimerRef.current)
|
||||
@ -83,36 +55,28 @@ const Question: FC<QuestionProps> = ({
|
||||
setIsEditing(false)
|
||||
setEditedContent(content)
|
||||
}, [content])
|
||||
|
||||
const handleEditInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key !== 'Enter' || e.shiftKey)
|
||||
return
|
||||
|
||||
if (e.nativeEvent.isComposing)
|
||||
return
|
||||
|
||||
if (isComposingRef.current) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
handleResend()
|
||||
}, [handleResend])
|
||||
|
||||
const clearCompositionEndTimer = useCallback(() => {
|
||||
if (!compositionEndTimerRef.current)
|
||||
return
|
||||
|
||||
clearTimeout(compositionEndTimerRef.current)
|
||||
compositionEndTimerRef.current = null
|
||||
}, [])
|
||||
|
||||
const handleCompositionStart = useCallback(() => {
|
||||
clearCompositionEndTimer()
|
||||
isComposingRef.current = true
|
||||
}, [clearCompositionEndTimer])
|
||||
|
||||
const handleCompositionEnd = useCallback(() => {
|
||||
clearCompositionEndTimer()
|
||||
compositionEndTimerRef.current = setTimeout(() => {
|
||||
@ -120,7 +84,6 @@ const Question: FC<QuestionProps> = ({
|
||||
compositionEndTimerRef.current = null
|
||||
}, 50)
|
||||
}, [clearCompositionEndTimer])
|
||||
|
||||
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
|
||||
if (direction === 'prev') {
|
||||
if (item.prevSibling)
|
||||
@ -131,13 +94,11 @@ const Question: FC<QuestionProps> = ({
|
||||
switchSibling?.(item.nextSibling)
|
||||
}
|
||||
}, [switchSibling, item.prevSibling, item.nextSibling])
|
||||
|
||||
const getContentWidth = () => {
|
||||
/* v8 ignore next 2 -- @preserve */
|
||||
if (contentRef.current)
|
||||
setContentWidth(contentRef.current?.clientWidth)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
/* v8 ignore next 2 -- @preserve */
|
||||
if (!contentRef.current)
|
||||
@ -150,27 +111,21 @@ const Question: FC<QuestionProps> = ({
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearCompositionEndTimer()
|
||||
}
|
||||
}, [clearCompositionEndTimer])
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex justify-end last:mb-0">
|
||||
<div className={cn('group relative mr-4 flex max-w-full items-start overflow-x-hidden pl-14', isEditing && 'flex-1')}>
|
||||
<div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}>
|
||||
<div
|
||||
data-testid="action-container"
|
||||
className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex"
|
||||
style={{ right: contentWidth + 8 }}
|
||||
>
|
||||
<div data-testid="action-container" className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex" style={{ right: contentWidth + 8 }}>
|
||||
<ActionButton
|
||||
data-testid="copy-btn"
|
||||
onClick={() => {
|
||||
copy(content)
|
||||
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
>
|
||||
<div className="i-ri-clipboard-line h-4 w-4" />
|
||||
@ -182,43 +137,14 @@ const Question: FC<QuestionProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-testid="question-content"
|
||||
className={cn(
|
||||
'w-full px-4 py-3 text-sm',
|
||||
!isEditing && 'rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 text-text-primary',
|
||||
isEditing && 'rounded-[24px] border-[3px] border-components-option-card-option-selected-border bg-components-panel-bg-blur shadow-lg',
|
||||
)}
|
||||
style={(!isEditing && theme?.chatBubbleColorStyle) ? CssTransform(theme.chatBubbleColorStyle) : {}}
|
||||
>
|
||||
{
|
||||
!!message_files?.length && (
|
||||
<FileList
|
||||
className={cn(isEditing ? 'mb-3' : 'mb-2')}
|
||||
files={message_files}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div ref={contentRef} data-testid="question-content" className={cn('w-full px-4 py-3 text-sm', !isEditing && 'rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 text-text-primary', isEditing && 'rounded-[24px] border-[3px] border-components-option-card-option-selected-border bg-components-panel-bg-blur shadow-lg')} style={(!isEditing && theme?.chatBubbleColorStyle) ? CssTransform(theme.chatBubbleColorStyle) : {}}>
|
||||
{!!message_files?.length && (<FileList className={cn(isEditing ? 'mb-3' : 'mb-2')} files={message_files} showDeleteAction={false} showDownloadAction={true} />)}
|
||||
{!isEditing
|
||||
? <Markdown content={content} />
|
||||
: (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="max-h-[158px] overflow-y-auto overflow-x-hidden pr-1">
|
||||
<Textarea
|
||||
className={cn(
|
||||
'w-full resize-none bg-transparent p-0 leading-7 text-text-primary outline-none body-lg-regular',
|
||||
)}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
value={editedContent}
|
||||
onChange={e => setEditedContent(e.target.value)}
|
||||
onKeyDown={handleEditInputKeyDown}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
/>
|
||||
<Textarea className={cn('w-full resize-none bg-transparent p-0 leading-7 text-text-primary outline-none body-lg-regular')} autoFocus minRows={1} value={editedContent} onChange={e => setEditedContent(e.target.value)} onKeyDown={handleEditInputKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} />
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button className="min-w-24" onClick={handleCancelEditing} data-testid="cancel-edit-btn">{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
@ -226,31 +152,20 @@ const Question: FC<QuestionProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<ContentSwitch
|
||||
count={item.siblingCount}
|
||||
currentIndex={item.siblingIndex}
|
||||
prevDisabled={!item.prevSibling}
|
||||
nextDisabled={!item.nextSibling}
|
||||
switchSibling={handleSwitchSibling}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (<ContentSwitch count={item.siblingCount} currentIndex={item.siblingIndex} prevDisabled={!item.prevSibling} nextDisabled={!item.nextSibling} switchSibling={handleSwitchSibling} />)}
|
||||
</div>
|
||||
<div className="mt-1 h-[18px]" />
|
||||
</div>
|
||||
{!hideAvatar && (
|
||||
<div className="h-10 w-10 shrink-0">
|
||||
{
|
||||
questionIcon || (
|
||||
<div className="h-full w-full rounded-full border-[0.5px] border-black/5">
|
||||
<div className="i-custom-public-avatar-user h-full w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{questionIcon || (
|
||||
<div className="h-full w-full rounded-full border-[0.5px] border-black/5">
|
||||
<div className="i-custom-public-avatar-user h-full w-full" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Question)
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ChatConfig } from '../../types'
|
||||
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import { ToastHost } from '@/app/components/base/ui/toast'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
AppSourceType,
|
||||
@ -109,7 +109,8 @@ const createQueryClient = () => new QueryClient({
|
||||
const createWrapper = (queryClient: QueryClient) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
<ToastHost />
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,38 +1,20 @@
|
||||
import type { ChatConfig, ChatItem, Feedback } from '../types'
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import type { InputValueTypes } from '@/app/components/share/text-generation/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import type {
|
||||
AppData,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import type { AppData, ConversationItem } from '@/models/share'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import { AppSourceType, updateFeedback } from '@/service/share'
|
||||
import {
|
||||
useInvalidateShareConversations,
|
||||
useShareChatList,
|
||||
useShareConversationName,
|
||||
useShareConversations,
|
||||
} from '@/service/use-share'
|
||||
import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share'
|
||||
import { useGetTryAppInfo, useGetTryAppParams } from '@/service/use-try-app'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||
@ -64,7 +46,6 @@ function getFormattedChatList(messages: any[]) {
|
||||
})
|
||||
return newChatList
|
||||
}
|
||||
|
||||
export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: string) => {
|
||||
const isInstalledApp = false // just can be webapp and try app
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
@ -75,17 +56,13 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
const { data: tryAppParams } = useGetTryAppParams(isTryApp ? tryAppId! : '')
|
||||
const webAppParams = useWebAppStore(s => s.appParams)
|
||||
const appParams = isTryApp ? tryAppParams : webAppParams
|
||||
|
||||
const appId = useMemo(() => {
|
||||
return isTryApp ? tryAppId : (appInfo as any)?.app_id
|
||||
}, [appInfo, isTryApp, tryAppId])
|
||||
|
||||
const embeddedConversationId = useWebAppStore(s => s.embeddedConversationId)
|
||||
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
|
||||
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [conversationId, setConversationId] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isTryApp)
|
||||
return
|
||||
@ -94,15 +71,12 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
setConversationId(conversation_id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setUserId(embeddedUserId || undefined)
|
||||
}, [embeddedUserId])
|
||||
|
||||
useEffect(() => {
|
||||
setConversationId(embeddedConversationId || undefined)
|
||||
}, [embeddedConversationId])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTryApp)
|
||||
return
|
||||
@ -110,11 +84,9 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
// Check URL parameters for language override
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const localeParam = urlParams.get('locale')
|
||||
|
||||
// Check for encoded system variables
|
||||
const systemVariables = await getProcessedSystemVariablesFromUrlParams()
|
||||
const localeFromSysVar = systemVariables.locale
|
||||
|
||||
if (localeParam) {
|
||||
// If locale parameter exists in URL, use it instead of default
|
||||
await changeLanguage(localeParam as Locale)
|
||||
@ -128,10 +100,8 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
await changeLanguage((appInfo as unknown as AppData).site?.default_language)
|
||||
}
|
||||
}
|
||||
|
||||
setLanguageFromParams()
|
||||
}, [appInfo])
|
||||
|
||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
||||
defaultValue: {},
|
||||
})
|
||||
@ -158,51 +128,36 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
})
|
||||
}
|
||||
}, [appId, conversationIdInfo, setConversationIdInfo, userId])
|
||||
|
||||
const [newConversationId, setNewConversationId] = useState('')
|
||||
const chatShouldReloadKey = useMemo(() => {
|
||||
if (currentConversationId === newConversationId)
|
||||
return ''
|
||||
|
||||
return currentConversationId
|
||||
}, [currentConversationId, newConversationId])
|
||||
|
||||
const { data: appPinnedConversationData } = useShareConversations({
|
||||
appSourceType,
|
||||
appId,
|
||||
pinned: true,
|
||||
limit: 100,
|
||||
})
|
||||
const {
|
||||
data: appConversationData,
|
||||
isLoading: appConversationDataLoading,
|
||||
} = useShareConversations({
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading } = useShareConversations({
|
||||
appSourceType,
|
||||
appId,
|
||||
pinned: false,
|
||||
limit: 100,
|
||||
})
|
||||
const {
|
||||
data: appChatListData,
|
||||
isLoading: appChatListDataLoading,
|
||||
} = useShareChatList({
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useShareChatList({
|
||||
conversationId: chatShouldReloadKey,
|
||||
appSourceType,
|
||||
appId,
|
||||
})
|
||||
const invalidateShareConversations = useInvalidateShareConversations()
|
||||
|
||||
const [clearChatList, setClearChatList] = useState(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const appPrevChatList = useMemo(
|
||||
() => (currentConversationId && appChatListData?.data.length)
|
||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||
: [],
|
||||
[appChatListData, currentConversationId],
|
||||
)
|
||||
|
||||
const appPrevChatList = useMemo(() => (currentConversationId && appChatListData?.data.length)
|
||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||
: [], [appChatListData, currentConversationId])
|
||||
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
||||
|
||||
const pinnedConversationList = useMemo(() => {
|
||||
return appPinnedConversationData?.data || []
|
||||
}, [appPinnedConversationData])
|
||||
@ -222,7 +177,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
let value = initInputs[item.paragraph.variable]
|
||||
if (value && item.paragraph.max_length && value.length > item.paragraph.max_length)
|
||||
value = value.slice(0, item.paragraph.max_length)
|
||||
|
||||
return {
|
||||
...item.paragraph,
|
||||
default: value || item.default || item.paragraph.default,
|
||||
@ -237,7 +191,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
type: 'number',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.checkbox) {
|
||||
const preset = initInputs[item.checkbox.variable] === true
|
||||
return {
|
||||
@ -246,7 +199,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.select) {
|
||||
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
|
||||
return {
|
||||
@ -255,32 +207,27 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
type: 'select',
|
||||
}
|
||||
}
|
||||
|
||||
if (item['file-list']) {
|
||||
return {
|
||||
...item['file-list'],
|
||||
type: 'file-list',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.file) {
|
||||
return {
|
||||
...item.file,
|
||||
type: 'file',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.json_object) {
|
||||
return {
|
||||
...item.json_object,
|
||||
type: 'json_object',
|
||||
}
|
||||
}
|
||||
|
||||
let value = initInputs[item['text-input'].variable]
|
||||
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
|
||||
value = value.slice(0, item['text-input'].max_length)
|
||||
|
||||
return {
|
||||
...item['text-input'],
|
||||
default: value || item.default || item['text-input'].default,
|
||||
@ -288,11 +235,9 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}
|
||||
})
|
||||
}, [initInputs, appParams])
|
||||
|
||||
const allInputsHidden = useMemo(() => {
|
||||
return inputsForms.length > 0 && inputsForms.every(item => item.hide === true)
|
||||
}, [inputsForms])
|
||||
|
||||
useEffect(() => {
|
||||
// init inputs from url params
|
||||
(async () => {
|
||||
@ -306,13 +251,11 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const conversationInputs: Record<string, InputValueTypes> = {}
|
||||
|
||||
inputsForms.forEach((item) => {
|
||||
conversationInputs[item.variable] = item.default || null
|
||||
})
|
||||
handleNewConversationInputsChange(conversationInputs)
|
||||
}, [handleNewConversationInputsChange, inputsForms])
|
||||
|
||||
const { data: newConversation } = useShareConversationName({
|
||||
conversationId: newConversationId,
|
||||
appSourceType,
|
||||
@ -324,12 +267,11 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||
useEffect(() => {
|
||||
if (appConversationData?.data && !appConversationDataLoading)
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
setOriginConversationList(appConversationData?.data)
|
||||
}, [appConversationData, appConversationDataLoading])
|
||||
const conversationList = useMemo(() => {
|
||||
const data = originConversationList.slice()
|
||||
|
||||
if (showNewConversationItemInList && data[0]?.id !== '') {
|
||||
data.unshift({
|
||||
id: '',
|
||||
@ -340,12 +282,10 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}
|
||||
return data
|
||||
}, [originConversationList, showNewConversationItemInList, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (newConversation) {
|
||||
setOriginConversationList(produce((draft) => {
|
||||
const index = draft.findIndex(item => item.id === newConversation.id)
|
||||
|
||||
if (index > -1)
|
||||
draft[index] = newConversation
|
||||
else
|
||||
@ -353,16 +293,12 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}))
|
||||
}
|
||||
}, [newConversation])
|
||||
|
||||
const currentConversationItem = useMemo(() => {
|
||||
let conversationItem = conversationList.find(item => item.id === currentConversationId)
|
||||
|
||||
if (!conversationItem && pinnedConversationList.length)
|
||||
conversationItem = pinnedConversationList.find(item => item.id === currentConversationId)
|
||||
|
||||
return conversationItem
|
||||
}, [conversationList, currentConversationId, pinnedConversationList])
|
||||
|
||||
const currentConversationLatestInputs = useMemo(() => {
|
||||
if (!currentConversationId || !appChatListData?.data.length)
|
||||
return newConversationInputsRef.current || {}
|
||||
@ -371,15 +307,12 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
const [currentConversationInputs, setCurrentConversationInputs] = useState<Record<string, any>>(currentConversationLatestInputs || {})
|
||||
useEffect(() => {
|
||||
if (currentConversationItem && !isTryApp)
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
setCurrentConversationInputs(currentConversationLatestInputs || {})
|
||||
}, [currentConversationItem, currentConversationLatestInputs])
|
||||
|
||||
const { notify } = useToastContext()
|
||||
const checkInputsRequired = useCallback((silent?: boolean) => {
|
||||
if (allInputsHidden)
|
||||
return true
|
||||
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
|
||||
@ -387,13 +320,10 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (fileIsUploading)
|
||||
return
|
||||
|
||||
if (!newConversationInputsRef.current[variable] && !silent)
|
||||
hasEmptyInput = label as string
|
||||
|
||||
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) {
|
||||
const files = newConversationInputsRef.current[variable]
|
||||
if (Array.isArray(files))
|
||||
@ -403,26 +333,25 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEmptyInput) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
if (fileIsUploading) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) })
|
||||
toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
}, [inputsForms, notify, t, allInputsHidden])
|
||||
}, [inputsForms, t, allInputsHidden])
|
||||
const handleStartChat = useCallback((callback?: () => void) => {
|
||||
if (checkInputsRequired()) {
|
||||
setShowNewConversationItemInList(true)
|
||||
callback?.()
|
||||
}
|
||||
}, [setShowNewConversationItemInList, checkInputsRequired])
|
||||
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: noop })
|
||||
const currentChatInstanceRef = useRef<{
|
||||
handleStop: () => void
|
||||
}>({ handleStop: noop })
|
||||
const handleChangeConversation = useCallback((conversationId: string) => {
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setNewConversationId('')
|
||||
@ -435,26 +364,22 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
setClearChatList(true)
|
||||
return
|
||||
}
|
||||
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setShowNewConversationItemInList(true)
|
||||
handleChangeConversation('')
|
||||
handleNewConversationInputsChange(await getProcessedInputsFromUrlParams())
|
||||
setClearChatList(true)
|
||||
}, [isTryApp, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||
|
||||
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
||||
setNewConversationId(newConversationId)
|
||||
handleConversationIdInfoChange(newConversationId)
|
||||
setShowNewConversationItemInList(false)
|
||||
invalidateShareConversations()
|
||||
}, [handleConversationIdInfoChange, invalidateShareConversations])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
}, [appSourceType, appId, t, notify])
|
||||
|
||||
toast.success(t('api.success', { ns: 'common' }))
|
||||
}, [appSourceType, appId, t])
|
||||
return {
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
|
||||
@ -16,8 +16,8 @@ vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
// Mock CodeEditor to trigger onChange easily
|
||||
|
||||
@ -28,7 +28,7 @@ vi.mock('@/service/annotation', () => ({
|
||||
addAnnotation: (...args: unknown[]) => mockAddAnnotation(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import ConfigParamModal from '../config-param-modal'
|
||||
|
||||
let mockHooksReturn: {
|
||||
@ -31,10 +31,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
ANNOTATION_DEFAULT: { score_threshold: 0.9 },
|
||||
}))
|
||||
@ -63,8 +59,11 @@ const defaultAnnotationConfig = {
|
||||
}
|
||||
|
||||
describe('ConfigParamModal', () => {
|
||||
const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
toastErrorSpy.mockClear()
|
||||
mockHooksReturn = {
|
||||
modelList: [{ provider: { provider: 'openai' }, models: [{ model: 'text-embedding-ada-002' }] }],
|
||||
defaultModel: { provider: { provider: 'openai' }, model: 'text-embedding-ada-002' },
|
||||
@ -241,7 +240,7 @@ describe('ConfigParamModal', () => {
|
||||
const saveBtn = buttons.find(b => b.textContent?.includes('initSetup'))
|
||||
fireEvent.click(saveBtn!)
|
||||
|
||||
expect(Toast.notify).toHaveBeenCalledWith(
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiEditLine,
|
||||
RiFileEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { RiEditLine, RiFileEditLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { addAnnotation } from '@/service/annotation'
|
||||
@ -22,16 +19,7 @@ type Props = {
|
||||
onAdded: (annotationId: string, authorName: string) => void
|
||||
onEdit: () => void
|
||||
}
|
||||
|
||||
const AnnotationCtrlButton: FC<Props> = ({
|
||||
cached,
|
||||
query,
|
||||
answer,
|
||||
appId,
|
||||
messageId,
|
||||
onAdded,
|
||||
onEdit,
|
||||
}) => {
|
||||
const AnnotationCtrlButton: FC<Props> = ({ cached, query, answer, appId, messageId, onAdded, onEdit }) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
@ -46,28 +34,20 @@ const AnnotationCtrlButton: FC<Props> = ({
|
||||
question: query,
|
||||
answer,
|
||||
})
|
||||
Toast.notify({
|
||||
message: t('api.actionSuccess', { ns: 'common' }) as string,
|
||||
type: 'success',
|
||||
})
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }) as string)
|
||||
onAdded(res.id, res.account?.name ?? '')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{cached && (
|
||||
<Tooltip
|
||||
popupContent={t('feature.annotation.edit', { ns: 'appDebug' })}
|
||||
>
|
||||
<Tooltip popupContent={t('feature.annotation.edit', { ns: 'appDebug' })}>
|
||||
<ActionButton onClick={onEdit}>
|
||||
<RiEditLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!cached && answer && (
|
||||
<Tooltip
|
||||
popupContent={t('feature.annotation.add', { ns: 'appDebug' })}
|
||||
>
|
||||
<Tooltip popupContent={t('feature.annotation.add', { ns: 'appDebug' })}>
|
||||
<ActionButton onClick={handleAdd}>
|
||||
<RiFileEditLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
|
||||
@ -6,7 +6,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
@ -25,22 +25,10 @@ type Props = {
|
||||
isInit?: boolean
|
||||
annotationConfig: AnnotationReplyConfig
|
||||
}
|
||||
|
||||
const ConfigParamModal: FC<Props> = ({
|
||||
isShow,
|
||||
onHide: doHide,
|
||||
onSave,
|
||||
isInit,
|
||||
annotationConfig: oldAnnotationConfig,
|
||||
}) => {
|
||||
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 { 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
|
||||
? {
|
||||
@ -57,13 +45,9 @@ const ConfigParamModal: FC<Props> = ({
|
||||
if (!isLoading)
|
||||
doHide()
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!embeddingModel || !embeddingModel.modelName || (embeddingModel.modelName === embeddingsDefaultModel?.model && !isEmbeddingsDefaultModelValid)) {
|
||||
Toast.notify({
|
||||
message: t('modelProvider.embeddingModel.required', { ns: 'common' }),
|
||||
type: 'error',
|
||||
})
|
||||
toast.error(t('modelProvider.embeddingModel.required', { ns: 'common' }))
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
@ -73,22 +57,14 @@ const ConfigParamModal: FC<Props> = ({
|
||||
}, annotationConfig.score_threshold)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
className="!mt-14 !w-[640px] !max-w-none !p-6"
|
||||
>
|
||||
<Modal isShow={isShow} onClose={onHide} className="!mt-14 !w-[640px] !max-w-none !p-6">
|
||||
<div className="mb-2 text-text-primary title-2xl-semi-bold">
|
||||
{t(`initSetup.${isInit ? 'title' : 'configTitle'}`, { ns: 'appAnnotation' })}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-3">
|
||||
<Item
|
||||
title={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })}
|
||||
tooltip={t('feature.annotation.scoreThreshold.description', { ns: 'appDebug' })}
|
||||
>
|
||||
<Item title={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })} tooltip={t('feature.annotation.scoreThreshold.description', { ns: 'appDebug' })}>
|
||||
<ScoreSlider
|
||||
className="mt-1"
|
||||
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
|
||||
@ -101,10 +77,7 @@ const ConfigParamModal: FC<Props> = ({
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item
|
||||
title={t('modelProvider.embeddingModel.key', { ns: 'common' })}
|
||||
tooltip={t('embeddingModelSwitchTip', { ns: 'appAnnotation' })}
|
||||
>
|
||||
<Item title={t('modelProvider.embeddingModel.key', { ns: 'common' })} tooltip={t('embeddingModelSwitchTip', { ns: 'appAnnotation' })}>
|
||||
<div className="pt-1">
|
||||
<ModelSelector
|
||||
defaultModel={embeddingModel && {
|
||||
@ -125,11 +98,7 @@ const ConfigParamModal: FC<Props> = ({
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Button variant="primary" onClick={handleSave} loading={isLoading}>
|
||||
<div></div>
|
||||
<div>{t(`initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`, { ns: 'appAnnotation' })}</div>
|
||||
</Button>
|
||||
|
||||
@ -4,8 +4,8 @@ import * as i18n from 'react-i18next'
|
||||
import ModerationSettingModal from '../moderation-setting-modal'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
|
||||
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -20,27 +20,19 @@ import FormGeneration from './form-generation'
|
||||
import ModerationContent from './moderation-content'
|
||||
|
||||
const systemTypes = ['openai_moderation', 'keywords', 'api']
|
||||
|
||||
type Provider = {
|
||||
key: string
|
||||
name: string
|
||||
form_schema?: CodeBasedExtensionItem['form_schema']
|
||||
}
|
||||
|
||||
type ModerationSettingModalProps = {
|
||||
data: ModerationConfig
|
||||
onCancel: () => void
|
||||
onSave: (moderationConfig: ModerationConfig) => void
|
||||
}
|
||||
|
||||
const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
data,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
const ModerationSettingModal: FC<ModerationSettingModalProps> = ({ data, onCancel, onSave }) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const locale = useLocale()
|
||||
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
@ -73,25 +65,20 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
key: 'api',
|
||||
name: t('apiBasedExtension.selector.title', { ns: 'common' }),
|
||||
},
|
||||
...(
|
||||
codeBasedExtensionList
|
||||
? codeBasedExtensionList.data.map((item) => {
|
||||
return {
|
||||
key: item.name,
|
||||
name: locale === 'zh-Hans' ? item.label['zh-Hans'] : item.label['en-US'],
|
||||
form_schema: item.form_schema,
|
||||
}
|
||||
})
|
||||
: []
|
||||
),
|
||||
...(codeBasedExtensionList
|
||||
? codeBasedExtensionList.data.map((item) => {
|
||||
return {
|
||||
key: item.name,
|
||||
name: locale === 'zh-Hans' ? item.label['zh-Hans'] : item.label['en-US'],
|
||||
form_schema: item.form_schema,
|
||||
}
|
||||
})
|
||||
: []),
|
||||
]
|
||||
|
||||
const currentProvider = providers.find(provider => provider.key === localeData.type)
|
||||
|
||||
const handleDataTypeChange = (type: string) => {
|
||||
let config: undefined | Record<string, any>
|
||||
const currProvider = providers.find(provider => provider.key === type)
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
|
||||
config = currProvider?.form_schema.reduce((prev, next) => {
|
||||
prev[next.variable] = next.default
|
||||
@ -104,19 +91,15 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataKeywordsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
|
||||
const arr = value.split('\n').reduce((prev: string[], next: string) => {
|
||||
if (next !== '')
|
||||
prev.push(next.slice(0, 100))
|
||||
if (next === '' && prev[prev.length - 1] !== '')
|
||||
prev.push(next)
|
||||
|
||||
return prev
|
||||
}, [])
|
||||
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
config: {
|
||||
@ -125,7 +108,6 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
@ -135,7 +117,6 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
@ -145,7 +126,6 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataExtraChange = (extraValue: Record<string, string>) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
@ -155,24 +135,19 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const formatData = (originData: ModerationConfig) => {
|
||||
const { enabled, type, config } = originData
|
||||
const { inputs_config, outputs_config } = config!
|
||||
const params: Record<string, string | undefined> = {}
|
||||
|
||||
if (type === 'keywords')
|
||||
params.keywords = config?.keywords
|
||||
|
||||
if (type === 'api')
|
||||
params.api_based_extension_id = config?.api_based_extension_id
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currentProvider?.form_schema) {
|
||||
currentProvider.form_schema.forEach((form) => {
|
||||
params[form.variable] = config?.[form.variable]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
enabled,
|
||||
@ -183,58 +158,42 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
/* v8 ignore next -- UI-invariant guard: same condition is used in Save button disabled logic, so when true handleSave has no user-triggerable invocation path. @preserve */
|
||||
if (localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
|
||||
return
|
||||
|
||||
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
|
||||
notify({ type: 'error', message: t('feature.moderation.modal.content.condition', { ns: 'appDebug' }) })
|
||||
toast.error(t('feature.moderation.modal.content.condition', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'keywords' && !localeData.config.keywords) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
|
||||
notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) })
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
|
||||
for (let i = 0; i < currentProvider.form_schema.length; i++) {
|
||||
if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
|
||||
})
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
|
||||
notify({ type: 'error', message: t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }) })
|
||||
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
|
||||
notify({ type: 'error', message: t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }) })
|
||||
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
onSave(formatData(localeData))
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={noop}
|
||||
className="!mt-14 !w-[600px] !max-w-none !p-6"
|
||||
>
|
||||
<Modal isShow onClose={noop} className="!mt-14 !w-[600px] !max-w-none !p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-text-primary title-2xl-semi-bold">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
|
||||
<div
|
||||
@ -257,139 +216,74 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{
|
||||
providers.map(provider => (
|
||||
<div
|
||||
key={provider.key}
|
||||
className={cn(
|
||||
'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary system-sm-regular',
|
||||
localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs system-sm-medium',
|
||||
localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
|
||||
)}
|
||||
onClick={() => handleDataTypeChange(provider.key)}
|
||||
>
|
||||
<div className={cn(
|
||||
'mr-2 h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
|
||||
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
|
||||
)}
|
||||
>
|
||||
</div>
|
||||
{provider.name}
|
||||
{providers.map(provider => (
|
||||
<div key={provider.key} className={cn('flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary system-sm-regular', localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs', localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs system-sm-medium', localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled')} onClick={() => handleDataTypeChange(provider.key)}>
|
||||
<div className={cn('mr-2 h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs', localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked')}>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{provider.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{
|
||||
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
|
||||
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
|
||||
<span className="i-custom-vender-line-general-info-circle mr-1 h-4 w-4 text-[#F79009]" />
|
||||
<div className="flex items-center text-xs font-medium text-gray-700">
|
||||
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
|
||||
<span
|
||||
className="cursor-pointer text-primary-600"
|
||||
onClick={handleOpenSettingsModal}
|
||||
>
|
||||
{!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
|
||||
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
|
||||
<span className="i-custom-vender-line-general-info-circle mr-1 h-4 w-4 text-[#F79009]" />
|
||||
<div className="flex items-center text-xs font-medium text-gray-700">
|
||||
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
|
||||
<span className="cursor-pointer text-primary-600" onClick={handleOpenSettingsModal}>
|
||||
|
||||
{t('settings.provider', { ns: 'common' })}
|
||||
{t('settings.provider', { ns: 'common' })}
|
||||
|
||||
</span>
|
||||
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
|
||||
</div>
|
||||
</span>
|
||||
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
localeData.type === 'keywords' && (
|
||||
<div className="py-2">
|
||||
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
<div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2">
|
||||
<textarea
|
||||
value={localeData.config?.keywords || ''}
|
||||
onChange={handleDataKeywordsChange}
|
||||
className="block h-full w-full resize-none appearance-none bg-transparent text-sm text-text-secondary outline-none"
|
||||
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">
|
||||
100
|
||||
{t('feature.moderation.modal.keywords.line', { ns: 'appDebug' })}
|
||||
</span>
|
||||
</div>
|
||||
{localeData.type === 'keywords' && (
|
||||
<div className="py-2">
|
||||
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
<div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2">
|
||||
<textarea value={localeData.config?.keywords || ''} onChange={handleDataKeywordsChange} className="block h-full w-full resize-none appearance-none bg-transparent text-sm text-text-secondary outline-none" placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''} />
|
||||
<div className="absolute bottom-2 right-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">
|
||||
100
|
||||
{t('feature.moderation.modal.keywords.line', { ns: 'appDebug' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
localeData.type === 'api' && (
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
<a
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
|
||||
>
|
||||
<span className="i-custom-vender-line-education-book-open-01 mr-1 h-3 w-3 text-text-tertiary group-hover:text-primary-600" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
</a>
|
||||
</div>
|
||||
<ApiBasedExtensionSelector
|
||||
value={localeData.config?.api_based_extension_id || ''}
|
||||
onChange={handleDataApiBasedChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{localeData.type === 'api' && (
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
<a href={docLink('/use-dify/workspace/api-extension/api-extension')} target="_blank" rel="noopener noreferrer" className="group flex items-center text-xs text-text-tertiary hover:text-primary-600">
|
||||
<span className="i-custom-vender-line-education-book-open-01 mr-1 h-3 w-3 text-text-tertiary group-hover:text-primary-600" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
<ApiBasedExtensionSelector value={localeData.config?.api_based_extension_id || ''} onChange={handleDataApiBasedChange} />
|
||||
</div>
|
||||
)}
|
||||
{systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
&& (<FormGeneration forms={currentProvider?.form_schema} value={localeData.config} onChange={handleDataExtraChange} />)}
|
||||
<Divider bgStyle="gradient" className="my-3 h-px" />
|
||||
<ModerationContent
|
||||
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('inputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<ModerationContent
|
||||
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('outputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<ModerationContent title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''} config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }} onConfigChange={config => handleDataContentChange('inputs_config', config)} info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''} showPreset={localeData.type !== 'api'} />
|
||||
<ModerationContent title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''} config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }} onConfigChange={config => handleDataContentChange('outputs_config', config)} info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''} showPreset={localeData.type !== 'api'} />
|
||||
<div className="mb-8 mt-1 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className="mr-2"
|
||||
>
|
||||
<Button onClick={onCancel} className="mr-2">
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
|
||||
>
|
||||
<Button variant="primary" onClick={handleSave} disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModerationSettingModal
|
||||
|
||||
@ -10,11 +10,8 @@ vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({ token: undefined }),
|
||||
}))
|
||||
|
||||
// Exception: hook requires toast context that isn't available without a provider wrapper
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
|
||||
}))
|
||||
|
||||
const mockSetFiles = vi.fn()
|
||||
|
||||
@ -4,7 +4,7 @@ import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { useState } from 'react'
|
||||
import { fn } from 'storybook/test'
|
||||
import { PreviewMode } from '@/app/components/base/features/types'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import { ToastHost } from '@/app/components/base/ui/toast'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import FileUploaderInAttachmentWrapper from './index'
|
||||
@ -83,7 +83,8 @@ const AttachmentDemo = (props: React.ComponentProps<typeof FileUploaderInAttachm
|
||||
const [files, setFiles] = useState<FileEntity[]>(mockFiles)
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<>
|
||||
<ToastHost />
|
||||
<div className="w-[320px] rounded-2xl border border-divider-subtle bg-components-panel-bg p-4 shadow-xs">
|
||||
<FileUploaderInAttachmentWrapper
|
||||
{...props}
|
||||
@ -91,7 +92,7 @@ const AttachmentDemo = (props: React.ComponentProps<typeof FileUploaderInAttachm
|
||||
onChange={setFiles}
|
||||
/>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { FileEntity } from '../types'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { useState } from 'react'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import { ToastHost } from '@/app/components/base/ui/toast'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import FileUploaderInChatInput from '.'
|
||||
@ -36,7 +36,8 @@ const ChatInputDemo = ({ initialFiles = mockFiles, ...props }: ChatInputDemoProp
|
||||
const [files, setFiles] = useState<FileEntity[]>(initialFiles)
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<>
|
||||
<ToastHost />
|
||||
<FileContextProvider value={files} onChange={setFiles}>
|
||||
<div className="w-[360px] rounded-2xl border border-divider-subtle bg-components-panel-bg p-4">
|
||||
<div className="mb-3 text-xs text-text-secondary">Simulated chat input</div>
|
||||
@ -49,7 +50,7 @@ const ChatInputDemo = ({ initialFiles = mockFiles, ...props }: ChatInputDemoProp
|
||||
</div>
|
||||
</div>
|
||||
</FileContextProvider>
|
||||
</ToastProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user