{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}
{`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}
+ {more.tokens_per_second && (
+
{more.time}
diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx
index fca0ae5cae..d068d3e108 100644
--- a/web/app/components/base/chat/chat/answer/operation.tsx
+++ b/web/app/components/base/chat/chat/answer/operation.tsx
@@ -26,7 +26,7 @@ import NewAudioButton from '@/app/components/base/new-audio-button'
import Modal from '@/app/components/base/modal/modal'
import Textarea from '@/app/components/base/textarea'
import Tooltip from '@/app/components/base/tooltip'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type OperationProps = {
item: ChatItem
diff --git a/web/app/components/base/chat/chat/answer/tool-detail.tsx b/web/app/components/base/chat/chat/answer/tool-detail.tsx
index 26d1b3bbef..6e6710e053 100644
--- a/web/app/components/base/chat/chat/answer/tool-detail.tsx
+++ b/web/app/components/base/chat/chat/answer/tool-detail.tsx
@@ -7,7 +7,7 @@ import {
RiLoader2Line,
} from '@remixicon/react'
import type { ToolInfoInThought } from '../type'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type ToolDetailProps = {
payload: ToolInfoInThought
diff --git a/web/app/components/base/chat/chat/answer/workflow-process.tsx b/web/app/components/base/chat/chat/answer/workflow-process.tsx
index 0537d3c58b..c36f2b8f72 100644
--- a/web/app/components/base/chat/chat/answer/workflow-process.tsx
+++ b/web/app/components/base/chat/chat/answer/workflow-process.tsx
@@ -10,7 +10,7 @@ import {
import { useTranslation } from 'react-i18next'
import type { ChatItem, WorkflowProcess } from '../../types'
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx
index 5004bb2a92..bea1b3890b 100644
--- a/web/app/components/base/chat/chat/chat-input-area/index.tsx
+++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx
@@ -16,7 +16,7 @@ import type { InputForm } from '../type'
import { useCheckInputsForms } from '../check-input-forms-hooks'
import { useTextAreaHeight } from './hooks'
import Operation from './operation'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { FileListInChatInput } from '@/app/components/base/file-uploader'
import { useFile } from '@/app/components/base/file-uploader/hooks'
import {
diff --git a/web/app/components/base/chat/chat/chat-input-area/operation.tsx b/web/app/components/base/chat/chat/chat-input-area/operation.tsx
index 014ca6651f..2c041be90b 100644
--- a/web/app/components/base/chat/chat/chat-input-area/operation.tsx
+++ b/web/app/components/base/chat/chat/chat-input-area/operation.tsx
@@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
import type { FileUpload } from '@/app/components/base/features/types'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type OperationProps = {
fileConfig?: FileUpload
diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts
index a10b359724..3729fd4a6d 100644
--- a/web/app/components/base/chat/chat/hooks.ts
+++ b/web/app/components/base/chat/chat/hooks.ts
@@ -318,6 +318,7 @@ export const useChat = (
return player
}
+
ssePost(
url,
{
@@ -393,6 +394,7 @@ export const useChat = (
time: formatTime(newResponseItem.created_at, 'hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2),
+ tokens_per_second: newResponseItem.provider_response_latency > 0 ? (newResponseItem.answer_tokens / newResponseItem.provider_response_latency).toFixed(2) : undefined,
},
// for agent log
conversationId: conversationId.current,
diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx
index 0e947f8137..19c7b0da52 100644
--- a/web/app/components/base/chat/chat/index.tsx
+++ b/web/app/components/base/chat/chat/index.tsx
@@ -26,7 +26,7 @@ import ChatInputArea from './chat-input-area'
import TryToAsk from './try-to-ask'
import { ChatContextProvider } from './context'
import type { InputForm } from './type'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { Emoji } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
diff --git a/web/app/components/base/chat/chat/loading-anim/index.tsx b/web/app/components/base/chat/chat/loading-anim/index.tsx
index 801c89fce7..90cda3da2d 100644
--- a/web/app/components/base/chat/chat/loading-anim/index.tsx
+++ b/web/app/components/base/chat/chat/loading-anim/index.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
export type ILoadingAnimProps = {
type: 'text' | 'avatar'
diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx
index 21b604b969..a36e7ee160 100644
--- a/web/app/components/base/chat/chat/question.tsx
+++ b/web/app/components/base/chat/chat/question.tsx
@@ -21,7 +21,7 @@ import { RiClipboardLine, RiEditLine } from '@remixicon/react'
import Toast from '../../toast'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import Textarea from 'react-textarea-autosize'
import Button from '../../button'
import { useChatContext } from './context'
diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts
index d4cf460884..98cc05dda4 100644
--- a/web/app/components/base/chat/chat/type.ts
+++ b/web/app/components/base/chat/chat/type.ts
@@ -8,6 +8,7 @@ export type MessageMore = {
time: string
tokens: number
latency: number | string
+ tokens_per_second?: number | string
}
export type FeedbackType = {
diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
index a07e6217b0..ebd2e2de14 100644
--- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
@@ -22,7 +22,7 @@ import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
import Avatar from '../../avatar'
diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx
index 48f6de5725..16e656171e 100644
--- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx
@@ -12,7 +12,7 @@ import ActionButton from '@/app/components/base/action-button'
import Divider from '@/app/components/base/divider'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import DifyLogo from '@/app/components/base/logo/dify-logo'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IHeaderProps = {
diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx
index 1553d1f153..d908e39787 100644
--- a/web/app/components/base/chat/embedded-chatbot/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/index.tsx
@@ -17,7 +17,7 @@ import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import Header from '@/app/components/base/chat/embedded-chatbot/header'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import DifyLogo from '@/app/components/base/logo/dify-logo'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
index 88472b5d8f..ac1017c619 100644
--- a/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
@@ -5,7 +5,7 @@ import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
import { useEmbeddedChatbotContext } from '../context'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
collapsed: boolean
diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
index d6c89864d9..9d2a6d9824 100644
--- a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
@@ -7,7 +7,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigge
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
iconColor?: string
diff --git a/web/app/components/base/checkbox-list/index.tsx b/web/app/components/base/checkbox-list/index.tsx
index ca8333a200..efb1b588d1 100644
--- a/web/app/components/base/checkbox-list/index.tsx
+++ b/web/app/components/base/checkbox-list/index.tsx
@@ -3,7 +3,7 @@ import Badge from '@/app/components/base/badge'
import Checkbox from '@/app/components/base/checkbox'
import SearchInput from '@/app/components/base/search-input'
import SearchMenu from '@/assets/search-menu.svg'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import Image from 'next/image'
import type { FC } from 'react'
import { useCallback, useMemo, useState } from 'react'
diff --git a/web/app/components/base/checkbox/index.spec.tsx b/web/app/components/base/checkbox/index.spec.tsx
index 7ef901aef5..e817f05afd 100644
--- a/web/app/components/base/checkbox/index.spec.tsx
+++ b/web/app/components/base/checkbox/index.spec.tsx
@@ -26,7 +26,7 @@ describe('Checkbox Component', () => {
})
it('handles click events when not disabled', () => {
- const onCheck = jest.fn()
+ const onCheck = vi.fn()
render(
)
const checkbox = screen.getByTestId('checkbox-test')
@@ -35,7 +35,7 @@ describe('Checkbox Component', () => {
})
it('does not handle click events when disabled', () => {
- const onCheck = jest.fn()
+ const onCheck = vi.fn()
render(
)
const checkbox = screen.getByTestId('checkbox-test')
diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx
index 9495292ea6..5d222f5723 100644
--- a/web/app/components/base/checkbox/index.tsx
+++ b/web/app/components/base/checkbox/index.tsx
@@ -1,5 +1,5 @@
import { RiCheckLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import IndeterminateIcon from './assets/indeterminate-icon'
type CheckboxProps = {
diff --git a/web/app/components/base/chip/index.tsx b/web/app/components/base/chip/index.tsx
index eeaf2b19c6..919f2e1ab1 100644
--- a/web/app/components/base/chip/index.tsx
+++ b/web/app/components/base/chip/index.tsx
@@ -1,7 +1,7 @@
import type { FC } from 'react'
import { useMemo, useState } from 'react'
import { RiArrowDownSLine, RiCheckLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
diff --git a/web/app/components/base/content-dialog/index.tsx b/web/app/components/base/content-dialog/index.tsx
index 5efab57a40..4367744f4d 100644
--- a/web/app/components/base/content-dialog/index.tsx
+++ b/web/app/components/base/content-dialog/index.tsx
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { Transition, TransitionChild } from '@headlessui/react'
-import classNames from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type ContentDialogProps = {
className?: string
@@ -23,24 +23,20 @@ const ContentDialog = ({
>
-
+ className)}>
{children}
diff --git a/web/app/components/base/corner-label/index.tsx b/web/app/components/base/corner-label/index.tsx
index 0807ed4659..25cd228ba5 100644
--- a/web/app/components/base/corner-label/index.tsx
+++ b/web/app/components/base/corner-label/index.tsx
@@ -1,5 +1,5 @@
import { Corner } from '../icons/src/vender/solid/shapes'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type CornerLabelProps = {
label: string
diff --git a/web/app/components/base/date-and-time-picker/calendar/item.tsx b/web/app/components/base/date-and-time-picker/calendar/item.tsx
index 7132d7bdfb..991ab84043 100644
--- a/web/app/components/base/date-and-time-picker/calendar/item.tsx
+++ b/web/app/components/base/date-and-time-picker/calendar/item.tsx
@@ -1,6 +1,6 @@
import React, { type FC } from 'react'
import type { CalendarItemProps } from '../types'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import dayjs from '../utils/dayjs'
const Item: FC
= ({
diff --git a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx
index 0144a7c6ec..fcb1e5299e 100644
--- a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx
+++ b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx
@@ -1,5 +1,5 @@
import React, { type FC, useEffect, useRef } from 'react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type OptionListItemProps = {
isSelected: boolean
diff --git a/web/app/components/base/date-and-time-picker/date-picker/footer.tsx b/web/app/components/base/date-and-time-picker/date-picker/footer.tsx
index 6351a8235b..9c7136f67a 100644
--- a/web/app/components/base/date-and-time-picker/date-picker/footer.tsx
+++ b/web/app/components/base/date-and-time-picker/date-picker/footer.tsx
@@ -2,7 +2,7 @@ import React, { type FC } from 'react'
import Button from '../../button'
import { type DatePickerFooterProps, ViewType } from '../types'
import { RiTimeLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
const Footer: FC = ({
diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
index db089d10d0..a0ccfa153d 100644
--- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { DatePickerProps, Period } from '../types'
import { ViewType } from '../types'
import type { Dayjs } from 'dayjs'
diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx
index 24c7fff52f..3c7226fb4b 100644
--- a/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx
+++ b/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx
@@ -5,7 +5,7 @@ import dayjs from '../utils/dayjs'
import { isDayjsObject } from '../utils/dayjs'
import type { TimePickerProps } from '../types'
-jest.mock('react-i18next', () => ({
+vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key === 'time.defaultPlaceholder') return 'Pick a time...'
@@ -17,7 +17,7 @@ jest.mock('react-i18next', () => ({
}),
}))
-jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
+vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children }: { children: React.ReactNode }) => {children}
,
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: (e: React.MouseEvent) => void }) => (
{children}
@@ -27,27 +27,22 @@ jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
),
}))
-jest.mock('./options', () => () => )
-jest.mock('./header', () => () => )
-jest.mock('@/app/components/base/timezone-label', () => {
- return function MockTimezoneLabel({ timezone, inline, className }: { timezone: string, inline?: boolean, className?: string }) {
- return (
-
- UTC+8
-
- )
- }
-})
+vi.mock('./options', () => ({
+ default: () => ,
+}))
+vi.mock('./header', () => ({
+ default: () => ,
+}))
describe('TimePicker', () => {
const baseProps: Pick = {
- onChange: jest.fn(),
- onClear: jest.fn(),
+ onChange: vi.fn(),
+ onClear: vi.fn(),
value: undefined,
}
beforeEach(() => {
- jest.clearAllMocks()
+ vi.clearAllMocks()
})
test('renders formatted value for string input (Issue #26692 regression)', () => {
@@ -86,7 +81,7 @@ describe('TimePicker', () => {
})
test('selecting current time emits timezone-aware value', () => {
- const onChange = jest.fn()
+ const onChange = vi.fn()
render(
{
/>,
)
- expect(screen.queryByTestId('timezone-label')).not.toBeInTheDocument()
+ expect(screen.queryByTitle(/Timezone: Asia\/Shanghai/)).not.toBeInTheDocument()
})
test('should not display timezone label when showTimezone is false', () => {
@@ -127,7 +122,7 @@ describe('TimePicker', () => {
/>,
)
- expect(screen.queryByTestId('timezone-label')).not.toBeInTheDocument()
+ expect(screen.queryByTitle(/Timezone: Asia\/Shanghai/)).not.toBeInTheDocument()
})
test('should display timezone label when showTimezone is true', () => {
@@ -140,23 +135,9 @@ describe('TimePicker', () => {
/>,
)
- const timezoneLabel = screen.getByTestId('timezone-label')
+ const timezoneLabel = screen.getByTitle(/Timezone: Asia\/Shanghai/)
expect(timezoneLabel).toBeInTheDocument()
- expect(timezoneLabel).toHaveAttribute('data-timezone', 'Asia/Shanghai')
- })
-
- test('should pass inline prop to timezone label', () => {
- render(
- ,
- )
-
- const timezoneLabel = screen.getByTestId('timezone-label')
- expect(timezoneLabel).toHaveAttribute('data-inline', 'true')
+ expect(timezoneLabel).toHaveTextContent(/UTC[+-]\d+/)
})
test('should not display timezone label when showTimezone is true but timezone is not provided', () => {
@@ -168,21 +149,7 @@ describe('TimePicker', () => {
/>,
)
- expect(screen.queryByTestId('timezone-label')).not.toBeInTheDocument()
- })
-
- test('should apply shrink-0 and text-xs classes to timezone label', () => {
- render(
- ,
- )
-
- const timezoneLabel = screen.getByTestId('timezone-label')
- expect(timezoneLabel).toHaveClass('shrink-0', 'text-xs')
+ expect(screen.queryByTitle(/Timezone:/)).not.toBeInTheDocument()
})
})
})
diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx
index 9577a107e5..316164bfac 100644
--- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx
@@ -18,7 +18,7 @@ import Options from './options'
import Header from './header'
import { useTranslation } from 'react-i18next'
import { RiCloseCircleFill, RiTimeLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import TimezoneLabel from '@/app/components/base/timezone-label'
const to24Hour = (hour12: string, period: Period) => {
diff --git a/web/app/components/base/dialog/index.tsx b/web/app/components/base/dialog/index.tsx
index d4c0f10b40..3a56942537 100644
--- a/web/app/components/base/dialog/index.tsx
+++ b/web/app/components/base/dialog/index.tsx
@@ -1,7 +1,7 @@
import { Fragment, useCallback } from 'react'
import type { ElementType, ReactNode } from 'react'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
-import classNames from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
// https://headlessui.com/react/dialog
@@ -35,37 +35,33 @@ const CustomDialog = ({