mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
use base ui toast
This commit is contained in:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user