Commit 134ece8c by Xianquan Committed by GitHub

feat: refine chat log feedback actions (#7172)

* fix: truncate long chat tool names

* feat: refine chat log feedback actions

* fix: type narrow log feedback actions
parent 0225519b
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
"log.feedback.mark_as_read": "Mark as Read", "log.feedback.mark_as_read": "Mark as Read",
"log.feedback.read": "Read", "log.feedback.read": "Read",
"log.feedback.show_feedback": "Show Feedback", "log.feedback.show_feedback": "Show Feedback",
"log.feedback.user_bad_feedback": "User feedback",
"master_agent_call": "Master agent", "master_agent_call": "Master agent",
"mobile_clear_history_confirm_tip": "This action cannot be undone", "mobile_clear_history_confirm_tip": "This action cannot be undone",
"mobile_clear_history_confirm_title": "Clear history?", "mobile_clear_history_confirm_title": "Clear history?",
......
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
"log.feedback.mark_as_read": "标为已读", "log.feedback.mark_as_read": "标为已读",
"log.feedback.read": "已读", "log.feedback.read": "已读",
"log.feedback.show_feedback": "显示反馈", "log.feedback.show_feedback": "显示反馈",
"log.feedback.user_bad_feedback": "用户反馈",
"master_agent_call": "主 agent 调用", "master_agent_call": "主 agent 调用",
"mobile_clear_history_confirm_tip": "此操作不可撤销", "mobile_clear_history_confirm_tip": "此操作不可撤销",
"mobile_clear_history_confirm_title": "确定清空历史记录?", "mobile_clear_history_confirm_title": "确定清空历史记录?",
......
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
"log.feedback.mark_as_read": "標為已讀", "log.feedback.mark_as_read": "標為已讀",
"log.feedback.read": "已讀", "log.feedback.read": "已讀",
"log.feedback.show_feedback": "顯示反饋", "log.feedback.show_feedback": "顯示反饋",
"log.feedback.user_bad_feedback": "用戶反饋",
"master_agent_call": "主 agent 調用", "master_agent_call": "主 agent 調用",
"mobile_clear_history_confirm_tip": "此操作不可撤銷", "mobile_clear_history_confirm_tip": "此操作不可撤銷",
"mobile_clear_history_confirm_title": "確定清空歷史記錄?", "mobile_clear_history_confirm_title": "確定清空歷史記錄?",
......
...@@ -8,6 +8,7 @@ import ChatController, { type ChatControllerProps } from '../ChatController'; ...@@ -8,6 +8,7 @@ import ChatController, { type ChatControllerProps } from '../ChatController';
import { ChatBoxContext } from '../../Provider'; import { ChatBoxContext } from '../../Provider';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatTypeEnum } from '../../constants'; import { ChatTypeEnum } from '../../constants';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ChatSiteItemType } from '../../type'; import type { ChatSiteItemType } from '../../type';
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils'; import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook'; import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook';
...@@ -37,7 +38,7 @@ const AIChatBubbleActions = ({ ...@@ -37,7 +38,7 @@ const AIChatBubbleActions = ({
responseData responseData
}: AIChatBubbleActionsProps) => { }: AIChatBubbleActionsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { onRetry } = chatControllerProps; const { onRetry, feedbackUserName } = chatControllerProps;
const { isPc } = useSystem(); const { isPc } = useSystem();
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType); const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showRetry = chatType !== ChatTypeEnum.log && !!onRetry; const showRetry = chatType !== ChatTypeEnum.log && !!onRetry;
...@@ -65,6 +66,11 @@ const AIChatBubbleActions = ({ ...@@ -65,6 +66,11 @@ const AIChatBubbleActions = ({
); );
}, [responseData]); }, [responseData]);
const showTotalPoints = showPoints && totalPoints > 0; const showTotalPoints = showPoints && totalPoints > 0;
const badFeedback = historyItem.obj === ChatRoleEnum.AI ? historyItem.userBadFeedback : undefined;
const isFeedbackRead =
historyItem.obj === ChatRoleEnum.AI ? historyItem.isFeedbackRead : undefined;
const showUnreadBadFeedback =
chatType === ChatTypeEnum.log && !!badFeedback && isFeedbackRead !== true;
const formattedPoints = useMemo(() => { const formattedPoints = useMemo(() => {
const formatted = new Intl.NumberFormat(undefined, { const formatted = new Intl.NumberFormat(undefined, {
...@@ -72,6 +78,22 @@ const AIChatBubbleActions = ({ ...@@ -72,6 +78,22 @@ const AIChatBubbleActions = ({
}).format(totalPoints); }).format(totalPoints);
return totalPoints > 0 ? `-${formatted}` : formatted; return totalPoints > 0 ? `-${formatted}` : formatted;
}, [totalPoints]); }, [totalPoints]);
const renderRunDetailAction = () => (
<Flex
alignItems={'center'}
gap={'4px'}
p={'4px'}
cursor={'pointer'}
color={'myGray.400'}
_hover={{ color: 'primary.600' }}
onClick={onOpenWholeModal}
>
<MyIcon name={'core/chat/terminal'} w={'16px'} />
<Box>{t('chat:run_detail')}</Box>
</Flex>
);
const showRunDetailAfterCopy =
chatControllerProps.footerRunDetailPosition === 'afterCopy' && showWholeResponse;
return ( return (
<Box mt={4} maxW={'100%'}> <Box mt={4} maxW={'100%'}>
...@@ -86,7 +108,11 @@ const AIChatBubbleActions = ({ ...@@ -86,7 +108,11 @@ const AIChatBubbleActions = ({
zIndex={1} zIndex={1}
> >
<Flex alignItems={'center'} gap={'4px'}> <Flex alignItems={'center'} gap={'4px'}>
<ChatController {...chatControllerProps} variant="footer" /> <ChatController
{...chatControllerProps}
variant="footer"
footerAfterCopySlot={showRunDetailAfterCopy ? renderRunDetailAction() : undefined}
/>
{showRetry && ( {showRetry && (
<MyTooltip label={t('common:core.chat.retry')}> <MyTooltip label={t('common:core.chat.retry')}>
...@@ -102,20 +128,7 @@ const AIChatBubbleActions = ({ ...@@ -102,20 +128,7 @@ const AIChatBubbleActions = ({
</MyTooltip> </MyTooltip>
)} )}
{showWholeResponse && ( {showWholeResponse && !showRunDetailAfterCopy && renderRunDetailAction()}
<Flex
alignItems={'center'}
gap={'4px'}
p={'4px'}
cursor={'pointer'}
color={'myGray.400'}
_hover={{ color: 'primary.600' }}
onClick={onOpenWholeModal}
>
<MyIcon name={'core/chat/terminal'} w={'16px'} />
<Box>{t('chat:run_detail')}</Box>
</Flex>
)}
{showSandboxAction && isPc && useAgentSandbox && ( {showSandboxAction && isPc && useAgentSandbox && (
<Flex <Flex
...@@ -157,6 +170,27 @@ const AIChatBubbleActions = ({ ...@@ -157,6 +170,27 @@ const AIChatBubbleActions = ({
)} )}
</Flex> </Flex>
{showUnreadBadFeedback && (
<Flex
mt={4}
flexDirection={'column'}
gap={'8px'}
maxW={'100%'}
border={'1px solid'}
borderColor={'myGray.250'}
borderRadius={'8px'}
p={'12px'}
whiteSpace={'pre-wrap'}
>
<Box fontSize={'10px'} lineHeight={'14px'} color={'myGray.500'}>
{feedbackUserName || t('chat:log.feedback.user_bad_feedback')}
</Box>
<Box fontSize={'12px'} lineHeight={'18px'} color={'myGray.900'}>
{badFeedback}
</Box>
</Flex>
)}
{questionGuides.length > 0 && ( {questionGuides.length > 0 && (
<Flex mt={4} flexDirection={'column'} alignItems={'flex-start'} gap={'8px'}> <Flex mt={4} flexDirection={'column'} alignItems={'flex-start'} gap={'8px'}>
{questionGuides.map((text) => ( {questionGuides.map((text) => (
......
...@@ -26,9 +26,11 @@ export type ChatControllerProps = { ...@@ -26,9 +26,11 @@ export type ChatControllerProps = {
onAddUserDislike?: () => void; onAddUserDislike?: () => void;
likeFeedbackEffectTrigger?: number; likeFeedbackEffectTrigger?: number;
onToggleFeedbackReadStatus?: () => void; onToggleFeedbackReadStatus?: () => void;
showFeedbackContent?: boolean;
onToggleFeedbackContent?: () => void;
variant?: 'panel' | 'footer'; variant?: 'panel' | 'footer';
disableFooterHoverTranslate?: boolean;
footerRunDetailPosition?: 'default' | 'afterCopy';
footerAfterCopySlot?: React.ReactNode;
feedbackUserName?: string;
}; };
const controlIconStyle = { const controlIconStyle = {
...@@ -63,9 +65,9 @@ const ChatController = ({ ...@@ -63,9 +65,9 @@ const ChatController = ({
onAddUserLike, onAddUserLike,
likeFeedbackEffectTrigger, likeFeedbackEffectTrigger,
onToggleFeedbackReadStatus, onToggleFeedbackReadStatus,
showFeedbackContent, variant = 'panel',
onToggleFeedbackContent, disableFooterHoverTranslate = false,
variant = 'panel' footerAfterCopySlot
}: ChatControllerProps & FlexProps) => { }: ChatControllerProps & FlexProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { copyData } = useCopyData(); const { copyData } = useCopyData();
...@@ -89,11 +91,16 @@ const ChatController = ({ ...@@ -89,11 +91,16 @@ const ChatController = ({
const renderTooltip = (label: string, children: React.ReactNode) => ( const renderTooltip = (label: string, children: React.ReactNode) => (
<MyTooltip label={label}>{children}</MyTooltip> <MyTooltip label={label}>{children}</MyTooltip>
); );
const iconStyle = isFooter ? footerIconStyle : controlIconStyle;
const getIconHoverStyle = (color: string) => ({ const getIconHoverStyle = (color: string) => ({
color, color,
...(isFooter ? { transform: 'translateY(-1px)' } : {}) ...(isFooter && !disableFooterHoverTranslate ? { transform: 'translateY(-1px)' } : {})
}); });
const iconStyle = isFooter
? {
...footerIconStyle,
_hover: getIconHoverStyle('primary.600')
}
: controlIconStyle;
const activeFeedbackStyle = isFooter const activeFeedbackStyle = isFooter
? { ? {
color: 'primary.600' color: 'primary.600'
...@@ -110,6 +117,23 @@ const ChatController = ({ ...@@ -110,6 +117,23 @@ const ChatController = ({
color: 'white', color: 'white',
bg: 'yellow.500' bg: 'yellow.500'
}; };
const showLogFeedbackAction =
isLogMode &&
chat.obj === ChatRoleEnum.AI &&
(!!chat.userGoodFeedback || !!chat.userBadFeedback);
const unreadFeedbackBadge =
showLogFeedbackAction && !chat.isFeedbackRead ? (
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
) : null;
const { const {
runAsync: requestOnToggleFeedbackReadStatus, runAsync: requestOnToggleFeedbackReadStatus,
...@@ -150,6 +174,7 @@ const ChatController = ({ ...@@ -150,6 +174,7 @@ const ChatController = ({
onClick={() => copyData(chatText)} onClick={() => copyData(chatText)}
/> />
)} )}
{isFooter && footerAfterCopySlot}
{!!onDelete && !isChatting && chatType !== 'log' && ( {!!onDelete && !isChatting && chatType !== 'log' && (
<> <>
{onRetry && {onRetry &&
...@@ -246,62 +271,77 @@ const ChatController = ({ ...@@ -246,62 +271,77 @@ const ChatController = ({
onClick={onMark} onClick={onMark}
/> />
)} )}
{chat.obj === ChatRoleEnum.AI && ( {showLogFeedbackAction && (
<> <Flex alignItems={'center'} gap={4}>
{/* 日志模式下,始终展示赞/踩 */} <Flex alignItems={'center'} gap={'4px'}>
{isLogMode ? (
<>
{!!chat.userGoodFeedback && ( {!!chat.userGoodFeedback && (
<MyTooltip label={t('chat:feedback_helpful')}> <MyTooltip label={t('chat:feedback_helpful')}>
<Box position={'relative'}> <Box position={'relative'} cursor={'not-allowed'}>
<MyIcon <MyIcon
{...iconStyle} {...iconStyle}
color={'green.500'}
name={'core/chat/feedback/goodLight'} name={'core/chat/feedback/goodLight'}
color={'primary.600'}
cursor={'not-allowed'} cursor={'not-allowed'}
pointerEvents={'none'}
_hover={{ color: 'primary.600' }}
/> />
{!chat.isFeedbackRead && ( {unreadFeedbackBadge}
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
)}
</Box> </Box>
</MyTooltip> </MyTooltip>
)} )}
{!!chat.userBadFeedback && ( {!!chat.userBadFeedback && (
<MyTooltip label={t('chat:feedback_unhelpful')}> <MyTooltip label={t('chat:feedback_unhelpful')}>
<Box position={'relative'}> <Box position={'relative'} cursor={'not-allowed'}>
<MyIcon <MyIcon
{...iconStyle} {...iconStyle}
color={'yellow.500'}
name={'core/chat/feedback/badLight'} name={'core/chat/feedback/badLight'}
color={'primary.600'}
cursor={'not-allowed'} cursor={'not-allowed'}
pointerEvents={'none'}
_hover={{ color: 'primary.600' }}
/> />
{!chat.isFeedbackRead && ( {unreadFeedbackBadge}
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
)}
</Box> </Box>
</MyTooltip> </MyTooltip>
)} )}
</> </Flex>
{onToggleFeedbackReadStatus &&
(chat.isFeedbackRead ? (
<Button
size={'xs'}
variant={'unstyled'}
display={'inline-flex'}
alignItems={'center'}
justifyContent={'center'}
px={2}
fontSize={'11px'}
h={'22px'}
color={'primary.600'}
borderRadius={'sm'}
_hover={{ bg: 'primary.50' }}
isLoading={isLoadingOnToggleFeedbackReadStatus}
onClick={requestOnToggleFeedbackReadStatus}
>
{t('chat:log.feedback.read')}
</Button>
) : ( ) : (
<Button
size={'xs'}
variant={'outline'}
color={'myGray.600'}
fontSize={'11px'}
h={'22px'}
isLoading={isLoadingOnToggleFeedbackReadStatus}
onClick={requestOnToggleFeedbackReadStatus}
>
{t('chat:log.feedback.mark_as_read')}
</Button>
))}
</Flex>
)}
{chat.obj === ChatRoleEnum.AI && !isLogMode && (
<> <>
{!!onAddUserLike && ( {!!onAddUserLike && (
<MyTooltip label={t('chat:feedback_helpful')}> <MyTooltip label={t('chat:feedback_helpful')}>
...@@ -310,6 +350,7 @@ const ChatController = ({ ...@@ -310,6 +350,7 @@ const ChatController = ({
{...iconStyle} {...iconStyle}
isActive={!!chat.userGoodFeedback} isActive={!!chat.userGoodFeedback}
effectTrigger={likeFeedbackEffectTrigger} effectTrigger={likeFeedbackEffectTrigger}
disableHoverTranslate={disableFooterHoverTranslate}
onClick={onAddUserLike} onClick={onAddUserLike}
/> />
) : ( ) : (
...@@ -346,53 +387,7 @@ const ChatController = ({ ...@@ -346,53 +387,7 @@ const ChatController = ({
)} )}
</> </>
)} )}
</>
)}
</Flex> </Flex>
{onToggleFeedbackReadStatus &&
chat.obj === ChatRoleEnum.AI &&
(chat.userGoodFeedback || chat.userBadFeedback) && (
<>
{chat.isFeedbackRead ? (
<Button
variant={'unstyled'}
alignItems={'center'}
fontSize={'xs'}
color={'myGray.500'}
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
isLoading={isLoadingOnToggleFeedbackReadStatus}
onClick={requestOnToggleFeedbackReadStatus}
>
{t('chat:log.feedback.read')}
</Button>
) : (
<Button
size={'xs'}
variant={'whitePrimaryOutline'}
fontSize={'xs'}
h={'22px'}
isLoading={isLoadingOnToggleFeedbackReadStatus}
onClick={requestOnToggleFeedbackReadStatus}
>
{t('chat:log.feedback.mark_as_read')}
</Button>
)}
{chat.userBadFeedback && onToggleFeedbackContent && !showFeedbackContent && (
<Button
size={'xs'}
variant={'grayGhost'}
fontSize={'xs'}
h={'22px'}
onClick={onToggleFeedbackContent}
color={'primary.600'}
>
{t('chat:log.feedback.show_feedback')}
</Button>
)}
</>
)}
</Flex> </Flex>
</> </>
); );
......
import { Box, type BoxProps, Button, Flex } from '@chakra-ui/react'; import { Box, type BoxProps, Flex } from '@chakra-ui/react';
import React, { useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import { type ChatControllerProps } from './ChatController'; import { type ChatControllerProps } from './ChatController';
import styles from '../index.module.scss'; import styles from '../index.module.scss';
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
...@@ -52,8 +52,6 @@ const ChatItem = (props: Props) => { ...@@ -52,8 +52,6 @@ const ChatItem = (props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showFeedbackContent, setShowFeedbackContent] = useState(false);
const styleMap: BoxProps = useMemoEnhance( const styleMap: BoxProps = useMemoEnhance(
() => ({ () => ({
order: chat.obj === ChatRoleEnum.Human ? 0 : 1, order: chat.obj === ChatRoleEnum.Human ? 0 : 1,
...@@ -259,36 +257,6 @@ const ChatItem = (props: Props) => { ...@@ -259,36 +257,6 @@ const ChatItem = (props: Props) => {
</Flex> </Flex>
)} )}
{/* User Feedback Content: Admin log show */}
{isChatLog &&
showFeedbackContent &&
chat.obj === ChatRoleEnum.AI &&
(chat.userGoodFeedback || chat.userBadFeedback) && (
<Box
mt={2}
maxW={'250'}
border={'1px solid'}
borderColor={'myGray.250'}
borderRadius={'md'}
p={3}
>
<Box fontSize={'sm'} color={'myGray.900'} whiteSpace={'pre-wrap'}>
{chat.userBadFeedback || chat.userGoodFeedback}
</Box>
<Flex justifyContent={'flex-end'} mt={2}>
<Button
size={'xs'}
variant={'grayGhost'}
fontSize={'xs'}
onClick={() => setShowFeedbackContent(false)}
color={'primary.600'}
>
{t('chat:log.feedback.hide_feedback')}
</Button>
</Flex>
</Box>
)}
{/* content */} {/* content */}
{splitAiResponseResults.map((value, i) => { {splitAiResponseResults.map((value, i) => {
const isPlanCard = const isPlanCard =
...@@ -352,9 +320,7 @@ const ChatItem = (props: Props) => { ...@@ -352,9 +320,7 @@ const ChatItem = (props: Props) => {
onOpenCiteModal={onOpenCiteModal} onOpenCiteModal={onOpenCiteModal}
chatControllerProps={{ chatControllerProps={{
...props, ...props,
isLastChild, isLastChild
showFeedbackContent,
onToggleFeedbackContent: () => setShowFeedbackContent(!showFeedbackContent)
}} }}
> >
{renderCommonFooter()} {renderCommonFooter()}
......
...@@ -42,6 +42,9 @@ export type ChatRecordsListProps = { ...@@ -42,6 +42,9 @@ export type ChatRecordsListProps = {
dataId: string; dataId: string;
trigger: number; trigger: number;
}; };
disableFooterHoverTranslate?: boolean;
footerRunDetailPosition?: 'default' | 'afterCopy';
feedbackUserName?: string;
onCloseCustomFeedback: ( onCloseCustomFeedback: (
chat: ChatSiteItemType, chat: ChatSiteItemType,
index: number index: number
...@@ -76,6 +79,9 @@ const ChatRecordsList = ({ ...@@ -76,6 +79,9 @@ const ChatRecordsList = ({
onAddUserLike, onAddUserLike,
onAddUserDislike, onAddUserDislike,
likeFeedbackEffect, likeFeedbackEffect,
disableFooterHoverTranslate,
footerRunDetailPosition,
feedbackUserName,
onCloseCustomFeedback, onCloseCustomFeedback,
onToggleFeedbackReadStatus onToggleFeedbackReadStatus
}: ChatRecordsListProps) => { }: ChatRecordsListProps) => {
...@@ -222,6 +228,9 @@ const ChatRecordsList = ({ ...@@ -222,6 +228,9 @@ const ChatRecordsList = ({
likeFeedbackEffect?.dataId === item.dataId likeFeedbackEffect?.dataId === item.dataId
? likeFeedbackEffect.trigger ? likeFeedbackEffect.trigger
: undefined, : undefined,
disableFooterHoverTranslate,
footerRunDetailPosition,
feedbackUserName,
onToggleFeedbackReadStatus: onToggleFeedbackReadStatus(item) onToggleFeedbackReadStatus: onToggleFeedbackReadStatus(item)
}} }}
> >
......
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, type BoxProps, type IconProps } from '@chakra-ui/react'; import { Box, type BoxProps, type IconProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import styles from '../index.module.scss'; import styles from '../index.module.scss';
...@@ -16,10 +16,12 @@ type Particle = { ...@@ -16,10 +16,12 @@ type Particle = {
color: string; color: string;
}; };
type LikeFeedbackButtonProps = Pick<BoxProps, 'cursor' | 'onClick'> & type LikeFeedbackButtonProps = Pick<BoxProps, 'cursor'> &
Pick<IconProps, 'w' | 'h' | 'boxSize' | 'p'> & { Pick<IconProps, 'w' | 'h' | 'boxSize' | 'p'> & {
isActive: boolean; isActive: boolean;
effectTrigger?: number; effectTrigger?: number;
disableHoverTranslate?: boolean;
onClick?: () => void;
}; };
const blueColors = ['#3370ff', '#4f82ff', '#7ca3ff']; const blueColors = ['#3370ff', '#4f82ff', '#7ca3ff'];
...@@ -50,7 +52,7 @@ const getParticles = (x: number, y: number): Particle[] => ...@@ -50,7 +52,7 @@ const getParticles = (x: number, y: number): Particle[] =>
/** /**
* 渲染点赞按钮的局部成功反馈。 * 渲染点赞按钮的局部成功反馈。
* *
* hover、图标弹跳和 canvas 粒子参数都对齐 prototype,只有新的 effectTrigger 会播放撒花。 * hover、点击图标弹跳和 canvas 粒子参数都对齐 prototype,只有新的 effectTrigger 会播放撒花。
*/ */
const LikeFeedbackButton = ({ const LikeFeedbackButton = ({
isActive, isActive,
...@@ -60,13 +62,15 @@ const LikeFeedbackButton = ({ ...@@ -60,13 +62,15 @@ const LikeFeedbackButton = ({
w, w,
h, h,
boxSize, boxSize,
p p,
disableHoverTranslate = false
}: LikeFeedbackButtonProps) => { }: LikeFeedbackButtonProps) => {
const buttonRef = useRef<HTMLSpanElement | null>(null); const buttonRef = useRef<HTMLSpanElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const particlesRef = useRef<Particle[]>([]); const particlesRef = useRef<Particle[]>([]);
const rafRef = useRef<number>(); const rafRef = useRef<number>();
const playedTriggerRef = useRef<number>(); const playedTriggerRef = useRef<number>();
const [iconPopTrigger, setIconPopTrigger] = useState(0);
const resizeCanvas = useCallback(() => { const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
...@@ -183,6 +187,13 @@ const LikeFeedbackButton = ({ ...@@ -183,6 +187,13 @@ const LikeFeedbackButton = ({
useEffect(() => stopAnimation, [stopAnimation]); useEffect(() => stopAnimation, [stopAnimation]);
const handleClick = useCallback(() => {
if (!onClick) return;
setIconPopTrigger((trigger) => trigger + 1);
onClick();
}, [onClick]);
return ( return (
<Box <Box
as="span" as="span"
...@@ -200,12 +211,12 @@ const LikeFeedbackButton = ({ ...@@ -200,12 +211,12 @@ const LikeFeedbackButton = ({
transition="color 180ms ease, transform 180ms ease, filter 180ms ease" transition="color 180ms ease, transform 180ms ease, filter 180ms ease"
_hover={{ _hover={{
color: 'primary.600', color: 'primary.600',
transform: 'translateY(-1px)' ...(!disableHoverTranslate && { transform: 'translateY(-1px)' })
}} }}
onClick={onClick} onClick={handleClick}
> >
<MyIcon <MyIcon
key={effectTrigger || 'idle'} key={iconPopTrigger}
w={w} w={w}
h={h} h={h}
boxSize={boxSize} boxSize={boxSize}
...@@ -213,7 +224,7 @@ const LikeFeedbackButton = ({ ...@@ -213,7 +224,7 @@ const LikeFeedbackButton = ({
cursor={undefined} cursor={undefined}
color="currentColor" color="currentColor"
_hover={undefined} _hover={undefined}
className={effectTrigger ? styles.likeFeedbackIconPop : undefined} className={iconPopTrigger > 0 ? styles.likeFeedbackIconPop : undefined}
name="core/chat/feedback/goodLight" name="core/chat/feedback/goodLight"
/> />
</Box> </Box>
......
...@@ -99,6 +99,12 @@ type Props = OutLinkChatAuthProps & ...@@ -99,6 +99,12 @@ type Props = OutLinkChatAuthProps &
EmptyState?: React.ReactNode; EmptyState?: React.ReactNode;
/** 是否启用 AI 正文 quick-replies 快捷回复渲染,默认关闭。 */ /** 是否启用 AI 正文 quick-replies 快捷回复渲染,默认关闭。 */
enableQuickReplies?: boolean; enableQuickReplies?: boolean;
/** 是否禁用 footer actions hover 时的上移动画。 */
disableFooterHoverTranslate?: boolean;
/** footer 中运行详情的位置,默认保持原有顺序。 */
footerRunDetailPosition?: 'default' | 'afterCopy';
/** 日志详情中展示用户反馈内容时使用的用户显示名。 */
feedbackUserName?: string;
}; };
const ChatBox = ({ const ChatBox = ({
...@@ -121,6 +127,9 @@ const ChatBox = ({ ...@@ -121,6 +127,9 @@ const ChatBox = ({
inputBodyProps, inputBodyProps,
EmptyState, EmptyState,
enableQuickReplies = false, enableQuickReplies = false,
disableFooterHoverTranslate = false,
footerRunDetailPosition = 'default',
feedbackUserName,
...props ...props
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -553,6 +562,9 @@ const ChatBox = ({ ...@@ -553,6 +562,9 @@ const ChatBox = ({
onAddUserLike, onAddUserLike,
onAddUserDislike, onAddUserDislike,
likeFeedbackEffect, likeFeedbackEffect,
disableFooterHoverTranslate,
footerRunDetailPosition,
feedbackUserName,
onCloseCustomFeedback, onCloseCustomFeedback,
onToggleFeedbackReadStatus onToggleFeedbackReadStatus
}), }),
...@@ -571,6 +583,9 @@ const ChatBox = ({ ...@@ -571,6 +583,9 @@ const ChatBox = ({
onAddUserLike, onAddUserLike,
onAddUserDislike, onAddUserDislike,
likeFeedbackEffect, likeFeedbackEffect,
disableFooterHoverTranslate,
footerRunDetailPosition,
feedbackUserName,
onCloseCustomFeedback, onCloseCustomFeedback,
onToggleFeedbackReadStatus onToggleFeedbackReadStatus
] ]
......
...@@ -44,8 +44,10 @@ const RenderTool = React.memo( ...@@ -44,8 +44,10 @@ const RenderTool = React.memo(
h={'24px'} h={'24px'}
minH={'24px'} minH={'24px'}
w={'fit-content'} w={'fit-content'}
maxW={'100%'}
display={'flex'} display={'flex'}
alignItems={'center'} alignItems={'center'}
overflow={'hidden'}
lineHeight={'24px'} lineHeight={'24px'}
p={0} p={0}
bg={'transparent'} bg={'transparent'}
...@@ -56,11 +58,19 @@ const RenderTool = React.memo( ...@@ -56,11 +58,19 @@ const RenderTool = React.memo(
_hover={{ bg: 'transparent', color: 'myGray.600' }} _hover={{ bg: 'transparent', color: 'myGray.600' }}
_expanded={{ color: 'myGray.600' }} _expanded={{ color: 'myGray.600' }}
> >
<HStack h={'24px'} lineHeight={'24px'} mr={1} spacing="0"> <HStack h={'24px'} lineHeight={'24px'} mr={1} spacing="0" minW={0} overflow={'hidden'}>
<Flex w="24px" h="24px" alignItems="center" justifyContent="center"> <Flex w="24px" h="24px" flexShrink={0} alignItems="center" justifyContent="center">
<Avatar src={tool.toolAvatar} w="16px" h="16px" borderRadius="xs" /> <Avatar src={tool.toolAvatar} w="16px" h="16px" borderRadius="xs" />
</Flex> </Flex>
<Box fontSize="16px" lineHeight="24px" color="myGray.600"> <Box
fontSize="16px"
lineHeight="24px"
color="myGray.600"
minW={0}
overflow={'hidden'}
textOverflow={'ellipsis'}
whiteSpace={'nowrap'}
>
{t(tool.toolName)} {t(tool.toolName)}
</Box> </Box>
</HStack> </HStack>
......
...@@ -10,7 +10,6 @@ import dynamic from 'next/dynamic'; ...@@ -10,7 +10,6 @@ import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants'; import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { PcHeader } from '@/pageComponents/chat/ChatHeader';
import { GetChatTypeEnum } from '@fastgpt/global/core/chat/constants'; import { GetChatTypeEnum } from '@fastgpt/global/core/chat/constants';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, { import ChatRecordContextProvider, {
...@@ -30,12 +29,14 @@ const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatB ...@@ -30,12 +29,14 @@ const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatB
type Props = { type Props = {
appId: string; appId: string;
chatId: string; chatId: string;
feedbackUserName?: string;
onClose: () => void; onClose: () => void;
}; };
const DetailLogsModal = ({ const DetailLogsModal = ({
appId, appId,
chatId, chatId,
feedbackUserName,
onClose, onClose,
feedbackRecordId, feedbackRecordId,
...@@ -47,7 +48,7 @@ const DetailLogsModal = ({ ...@@ -47,7 +48,7 @@ const DetailLogsModal = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const [refreshTrigger, setRefreshTrigger] = useState(false); const [, setRefreshTrigger] = useState(false);
const [feedbackType, setFeedbackType] = useState<'all' | 'has_feedback' | 'good' | 'bad'>('all'); const [feedbackType, setFeedbackType] = useState<'all' | 'has_feedback' | 'good' | 'bad'>('all');
const [unreadOnly, setUnreadOnly] = useState<boolean>(false); const [unreadOnly, setUnreadOnly] = useState<boolean>(false);
...@@ -59,7 +60,6 @@ const DetailLogsModal = ({ ...@@ -59,7 +60,6 @@ const DetailLogsModal = ({
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const { data: chat } = useRequest( const { data: chat } = useRequest(
async () => { async () => {
...@@ -77,14 +77,13 @@ const DetailLogsModal = ({ ...@@ -77,14 +77,13 @@ const DetailLogsModal = ({
{ {
manual: false, manual: false,
refreshDeps: [chatId], refreshDeps: [chatId],
onError(e) { onError() {
onClose(); onClose();
} }
} }
); );
const title = chat?.title; const title = chat?.title;
const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.workflowTool; const isPlugin = chat?.app.type === AppTypeEnum.workflowTool;
// Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染 // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
...@@ -146,42 +145,36 @@ const DetailLogsModal = ({ ...@@ -146,42 +145,36 @@ const DetailLogsModal = ({
/> />
</Flex> </Flex>
) : ( ) : (
<Flex <Flex alignItems={'center'} gap={2} px={[4, 5]} h={['48px', '56px']} color={'myGray.900'}>
alignItems={'center'}
px={[4, 5]}
h={['48px', '56px']}
borderBottom={'base'}
borderBottomColor={'myGray.200'}
color={'myGray.900'}
>
{isPc ? ( {isPc ? (
<> <Box
<PcHeader flex={'1 1 0'}
totalRecordsCount={totalRecordsCount} minW={0}
title={title || ''} className="textEllipsis"
chatModels={chatModels} fontSize={'16px'}
chatId={chatId} fontWeight={500}
/> lineHeight={'24px'}
<Box flex={1} /> >
</> {title}
</Box>
) : ( ) : (
<> <Flex px={3} alignItems={'center'} flex={'1 1 0'} w={0} justifyContent={'center'}>
<Flex px={3} alignItems={'center'} flex={'1 0 0'} w={0} justifyContent={'center'}>
<Box ml={1} className="textEllipsis"> <Box ml={1} className="textEllipsis">
{title} {title}
</Box> </Box>
</Flex> </Flex>
</>
)} )}
<SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} /> <SandboxEntryIcon size={'smSquare'} onOpen={onOpenSandboxModal} />
<IconButton <IconButton
variant={'whiteBase'} variant={'ghost'}
w={'32px'} w={'32px'}
h={'32px'} h={'32px'}
minW={'32px'} minW={'32px'}
p={0} p={0}
borderColor={'myGray.250'} bg={'transparent'}
border={'none'}
boxShadow={'none'}
aria-label="Close" aria-label="Close"
icon={<MyIcon name={'common/closeLight'} w={'16px'} />} icon={<MyIcon name={'common/closeLight'} w={'16px'} />}
onClick={onClose} onClick={onClose}
...@@ -206,6 +199,9 @@ const DetailLogsModal = ({ ...@@ -206,6 +199,9 @@ const DetailLogsModal = ({
showMarkIcon showMarkIcon
showVoiceIcon={false} showVoiceIcon={false}
chatType={ChatTypeEnum.log} chatType={ChatTypeEnum.log}
disableFooterHoverTranslate
footerRunDetailPosition={'afterCopy'}
feedbackUserName={feedbackUserName}
onTriggerRefresh={() => setRefreshTrigger((prev) => !prev)} onTriggerRefresh={() => setRefreshTrigger((prev) => !prev)}
/> />
)} )}
...@@ -234,7 +230,16 @@ const DetailLogsModal = ({ ...@@ -234,7 +230,16 @@ const DetailLogsModal = ({
</Flex> </Flex>
{/* Feedback filter bar - commented out, moved to Render component */} {/* Feedback filter bar - commented out, moved to Render component */}
<Flex bg="white" px={6} py={3} borderTop="1px solid" borderColor="myGray.200"> <Flex
bg="white"
mx={6}
py={6}
h={'85px'}
minH={'85px'}
flexShrink={0}
borderTop="1px solid"
borderColor="myGray.200"
>
<DetailLogsModalFeedbackTypeFilter <DetailLogsModalFeedbackTypeFilter
feedbackType={feedbackType} feedbackType={feedbackType}
setFeedbackType={setFeedbackType} setFeedbackType={setFeedbackType}
......
...@@ -275,7 +275,7 @@ export const DetailLogsModalFeedbackTypeFilter = ({ ...@@ -275,7 +275,7 @@ export const DetailLogsModalFeedbackTypeFilter = ({
return () => { return () => {
eventBus.off(EventNameEnum.refreshFeedback); eventBus.off(EventNameEnum.refreshFeedback);
}; };
}, []); }, [loadFeedbackRecords]);
return ( return (
<Flex alignItems={'center'} gap={3} w={'100%'}> <Flex alignItems={'center'} gap={3} w={'100%'}>
...@@ -290,17 +290,18 @@ export const DetailLogsModalFeedbackTypeFilter = ({ ...@@ -290,17 +290,18 @@ export const DetailLogsModalFeedbackTypeFilter = ({
{showNavigation && ( {showNavigation && (
<> <>
{/* Current position indicator */} {/* Current position indicator */}
<Box fontSize={'sm'} color={'myGray.600'} whiteSpace={'nowrap'} flex={1}> <Box fontSize={'sm'} color={'myGray.600'} whiteSpace={'nowrap'}>
{currentPosition}/{totalCount} {currentPosition}/{totalCount}
</Box> </Box>
{/* Previous button */} <Flex flex={1} gap={3} minW={0}>
<Button size="sm" w={'100px'} variant={'whiteBase'} onClick={handlePrev}> <Button size="sm" flex={1} h={'36px'} py={2} variant={'whiteBase'} onClick={handlePrev}>
{t('chat:Previous')} {t('chat:Previous')}
</Button> </Button>
<Button size="sm" w={'100px'} variant={'whiteBase'} onClick={handleNext}> <Button size="sm" flex={1} h={'36px'} py={2} variant={'whiteBase'} onClick={handleNext}>
{t('chat:Next')} {t('chat:Next')}
</Button> </Button>
</Flex>
</> </>
)} )}
</Flex> </Flex>
......
...@@ -73,7 +73,10 @@ const LogTable = ({ ...@@ -73,7 +73,10 @@ const LogTable = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const [detailLogsId, setDetailLogsId] = useState<string>(); const [detailLogData, setDetailLogData] = useState<{
chatId: string;
feedbackUserName?: string;
}>();
const appName = useContextSelector(AppContext, (v) => v.appDetail.name); const appName = useContextSelector(AppContext, (v) => v.appDetail.name);
const [unreadOnly, setUnreadOnly] = useState<boolean>(false); const [unreadOnly, setUnreadOnly] = useState<boolean>(false);
const [userIpType, setUserIpType] = useState<UserIpTypeValue>('all'); const [userIpType, setUserIpType] = useState<UserIpTypeValue>('all');
...@@ -392,13 +395,13 @@ const LogTable = ({ ...@@ -392,13 +395,13 @@ const LogTable = ({
<Flex gap={3} px={1}> <Flex gap={3} px={1}>
{!!item?.userGoodFeedbackCount && ( {!!item?.userGoodFeedbackCount && (
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<MyIcon mr={1} name={'core/chat/feedback/goodLight'} color={'green.500'} w={4} /> <MyIcon mr={1} name={'core/chat/feedback/goodLight'} color={'myGray.400'} w={4} />
{item.userGoodFeedbackCount} {item.userGoodFeedbackCount}
</Flex> </Flex>
)} )}
{!!item?.userBadFeedbackCount && ( {!!item?.userBadFeedbackCount && (
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<MyIcon mr={1} name={'core/chat/feedback/badLight'} color={'yellow.500'} w={4} /> <MyIcon mr={1} name={'core/chat/feedback/badLight'} color={'myGray.400'} w={4} />
{item.userBadFeedbackCount} {item.userBadFeedbackCount}
</Flex> </Flex>
)} )}
...@@ -569,7 +572,13 @@ const LogTable = ({ ...@@ -569,7 +572,13 @@ const LogTable = ({
key={item._id} key={item._id}
_hover={{ bg: 'myWhite.600' }} _hover={{ bg: 'myWhite.600' }}
cursor={'pointer'} cursor={'pointer'}
onClick={() => setDetailLogsId(item.chatId)} onClick={() =>
setDetailLogData({
chatId: item.chatId,
feedbackUserName:
item.outLinkUid || item.sourceMember?.name || item.tmbId || undefined
})
}
> >
<Td> <Td>
<HStack onClick={(e) => e.stopPropagation()}> <HStack onClick={(e) => e.stopPropagation()}>
...@@ -626,12 +635,13 @@ const LogTable = ({ ...@@ -626,12 +635,13 @@ const LogTable = ({
)} )}
</FloatingActionBar> </FloatingActionBar>
{!!detailLogsId && ( {!!detailLogData && (
<DetailLogsModal <DetailLogsModal
appId={appId} appId={appId}
chatId={detailLogsId} chatId={detailLogData.chatId}
feedbackUserName={detailLogData.feedbackUserName}
onClose={() => { onClose={() => {
setDetailLogsId(undefined); setDetailLogData(undefined);
getData(pageNum); getData(pageNum);
}} }}
/> />
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
...@@ -8,14 +8,13 @@ import { ...@@ -8,14 +8,13 @@ import {
GetFeedbackRecordIdsResponseSchema, GetFeedbackRecordIdsResponseSchema,
type GetFeedbackRecordIdsResponseType type GetFeedbackRecordIdsResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api'; } from '@fastgpt/global/openapi/core/chat/feedback/api';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
async function handler( async function handler(req: ApiRequestProps): Promise<GetFeedbackRecordIdsResponseType> {
req: ApiRequestProps, const { appId, chatId, feedbackType, unreadOnly } = parseApiInput({
_res: ApiResponseType<any> req,
): Promise<GetFeedbackRecordIdsResponseType> { bodySchema: GetFeedbackRecordIdsBodySchema
const { appId, chatId, feedbackType, unreadOnly } = GetFeedbackRecordIdsBodySchema.parse( }).body;
req.body
);
if (!appId || !chatId) { if (!appId || !chatId) {
return { return {
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authChatCrud } from '@/service/support/permission/auth/chat';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
...@@ -10,12 +10,13 @@ import { ...@@ -10,12 +10,13 @@ import {
} from '@fastgpt/global/openapi/core/chat/feedback/api'; } from '@fastgpt/global/openapi/core/chat/feedback/api';
import { updateChatFeedbackCount } from '@fastgpt/service/core/chat/controller'; import { updateChatFeedbackCount } from '@fastgpt/service/core/chat/controller';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
async function handler( async function handler(req: ApiRequestProps): Promise<UpdateFeedbackReadStatusResponseType> {
req: ApiRequestProps, const { appId, chatId, dataId, isRead } = parseApiInput({
_res: ApiResponseType<any> req,
): Promise<UpdateFeedbackReadStatusResponseType> { bodySchema: UpdateFeedbackReadStatusBodySchema
const { appId, chatId, dataId, isRead } = UpdateFeedbackReadStatusBodySchema.parse(req.body); }).body;
await authChatCrud({ await authChatCrud({
req, req,
......
...@@ -26,7 +26,7 @@ async function handler(req: ApiRequestProps): Promise<UpdateUserFeedbackResponse ...@@ -26,7 +26,7 @@ async function handler(req: ApiRequestProps): Promise<UpdateUserFeedbackResponse
...req.body ...req.body
}); });
const chatItem = await MongoChatItem.findOne({ appId, chatId, dataId }); const chatItem = await MongoChatItem.findOne({ appId, chatId, dataId, obj: ChatRoleEnum.AI });
if (!chatItem) { if (!chatItem) {
return Promise.reject('Chat item not found'); return Promise.reject('Chat item not found');
} }
...@@ -34,7 +34,7 @@ async function handler(req: ApiRequestProps): Promise<UpdateUserFeedbackResponse ...@@ -34,7 +34,7 @@ async function handler(req: ApiRequestProps): Promise<UpdateUserFeedbackResponse
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
// Update ChatItem feedback // Update ChatItem feedback
await MongoChatItem.updateOne( await MongoChatItem.updateOne(
{ appId, chatId, dataId }, { appId, chatId, dataId, obj: ChatRoleEnum.AI },
{ {
$unset: { $unset: {
...(userBadFeedback === undefined && { userBadFeedback: '' }), ...(userBadFeedback === undefined && { userBadFeedback: '' }),
......
...@@ -125,9 +125,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -125,9 +125,11 @@ describe('getFeedbackRecordIds api test', () => {
}); });
it('should return all good feedback records', async () => { it('should return all good feedback records', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -135,8 +137,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -135,8 +137,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'good', feedbackType: 'good',
unreadOnly: false unreadOnly: false
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(2); expect(res.data?.total).toBe(2);
...@@ -146,9 +147,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -146,9 +147,11 @@ describe('getFeedbackRecordIds api test', () => {
}); });
it('should return only unread good feedback records', async () => { it('should return only unread good feedback records', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -156,8 +159,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -156,8 +159,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'good', feedbackType: 'good',
unreadOnly: true unreadOnly: true
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(1); expect(res.data?.total).toBe(1);
...@@ -166,9 +168,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -166,9 +168,11 @@ describe('getFeedbackRecordIds api test', () => {
}); });
it('should return all bad feedback records', async () => { it('should return all bad feedback records', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -176,8 +180,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -176,8 +180,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'bad', feedbackType: 'bad',
unreadOnly: false unreadOnly: false
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(2); expect(res.data?.total).toBe(2);
...@@ -187,9 +190,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -187,9 +190,11 @@ describe('getFeedbackRecordIds api test', () => {
}); });
it('should return only unread bad feedback records', async () => { it('should return only unread bad feedback records', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -197,8 +202,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -197,8 +202,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'bad', feedbackType: 'bad',
unreadOnly: true unreadOnly: true
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(1); expect(res.data?.total).toBe(1);
...@@ -207,9 +211,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -207,9 +211,11 @@ describe('getFeedbackRecordIds api test', () => {
}); });
it('should return all feedback records with has_feedback type', async () => { it('should return all feedback records with has_feedback type', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -217,8 +223,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -217,8 +223,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'has_feedback', feedbackType: 'has_feedback',
unreadOnly: false unreadOnly: false
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(4); expect(res.data?.total).toBe(4);
...@@ -227,12 +232,15 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -227,12 +232,15 @@ describe('getFeedbackRecordIds api test', () => {
expect(res.data?.dataIds).toContain('data-2'); expect(res.data?.dataIds).toContain('data-2');
expect(res.data?.dataIds).toContain('data-3'); expect(res.data?.dataIds).toContain('data-3');
expect(res.data?.dataIds).toContain('data-4'); expect(res.data?.dataIds).toContain('data-4');
expect(res.data?.dataIds).not.toContain('data-6');
}); });
it('should return only unread feedback records with has_feedback type', async () => { it('should return only unread feedback records with has_feedback type', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -240,8 +248,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -240,8 +248,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'has_feedback', feedbackType: 'has_feedback',
unreadOnly: true unreadOnly: true
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(2); expect(res.data?.total).toBe(2);
...@@ -251,9 +258,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -251,9 +258,11 @@ describe('getFeedbackRecordIds api test', () => {
}); });
it('should return empty result when no appId or chatId', async () => { it('should return empty result when no appId or chatId', async () => {
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId: '', appId: '',
...@@ -261,8 +270,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -261,8 +270,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'good', feedbackType: 'good',
unreadOnly: false unreadOnly: false
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data?.total).toBe(0); expect(res.data?.total).toBe(0);
...@@ -272,9 +280,11 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -272,9 +280,11 @@ describe('getFeedbackRecordIds api test', () => {
it('should fail when user does not have permission', async () => { it('should fail when user does not have permission', async () => {
const unauthorizedUser = await getUser(`unauthorized-user-get-ids-${Math.random()}`); const unauthorizedUser = await getUser(`unauthorized-user-get-ids-${Math.random()}`);
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>( const res = await Call<
handler, GetFeedbackRecordIdsBodyType,
{ Record<string, never>,
GetFeedbackRecordIdsResponseType
>(handler, {
auth: unauthorizedUser, auth: unauthorizedUser,
body: { body: {
appId, appId,
...@@ -282,8 +292,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -282,8 +292,7 @@ describe('getFeedbackRecordIds api test', () => {
feedbackType: 'good', feedbackType: 'good',
unreadOnly: false unreadOnly: false
} }
} });
);
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
......
...@@ -69,7 +69,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -69,7 +69,7 @@ describe('updateFeedbackReadStatus api test', () => {
it('should mark feedback as read', async () => { it('should mark feedback as read', async () => {
const res = await Call< const res = await Call<
UpdateFeedbackReadStatusBodyType, UpdateFeedbackReadStatusBodyType,
{}, Record<string, never>,
UpdateFeedbackReadStatusResponseType UpdateFeedbackReadStatusResponseType
>(handler, { >(handler, {
auth: testUser, auth: testUser,
...@@ -101,7 +101,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -101,7 +101,7 @@ describe('updateFeedbackReadStatus api test', () => {
const res = await Call< const res = await Call<
UpdateFeedbackReadStatusBodyType, UpdateFeedbackReadStatusBodyType,
{}, Record<string, never>,
UpdateFeedbackReadStatusResponseType UpdateFeedbackReadStatusResponseType
>(handler, { >(handler, {
auth: testUser, auth: testUser,
...@@ -132,7 +132,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -132,7 +132,7 @@ describe('updateFeedbackReadStatus api test', () => {
const res = await Call< const res = await Call<
UpdateFeedbackReadStatusBodyType, UpdateFeedbackReadStatusBodyType,
{}, Record<string, never>,
UpdateFeedbackReadStatusResponseType UpdateFeedbackReadStatusResponseType
>(handler, { >(handler, {
auth: unauthorizedUser, auth: unauthorizedUser,
...@@ -149,6 +149,82 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -149,6 +149,82 @@ describe('updateFeedbackReadStatus api test', () => {
}); });
it('should only update AI role chat items', async () => { it('should only update AI role chat items', async () => {
const sharedDataId = getNanoid();
await MongoChatItem.create([
{
teamId: testUser.teamId,
tmbId: testUser.tmbId,
userId: testUser.userId,
appId,
chatId,
dataId: sharedDataId,
obj: ChatRoleEnum.Human,
value: [
{
type: 'text',
text: {
content: 'Test question'
}
}
],
userBadFeedback: 'Human feedback should stay unchanged',
isFeedbackRead: false
},
{
teamId: testUser.teamId,
tmbId: testUser.tmbId,
userId: testUser.userId,
appId,
chatId,
dataId: sharedDataId,
obj: ChatRoleEnum.AI,
value: [
{
type: 'text',
text: {
content: 'Test response'
}
}
],
isFeedbackRead: false
}
]);
const res = await Call<
UpdateFeedbackReadStatusBodyType,
Record<string, never>,
UpdateFeedbackReadStatusResponseType
>(handler, {
auth: testUser,
body: {
appId,
chatId,
dataId: sharedDataId,
isRead: true
}
});
expect(res.code).toBe(200);
const humanChatItem = await MongoChatItem.findOne({
appId,
chatId,
dataId: sharedDataId,
obj: ChatRoleEnum.Human
});
const aiChatItem = await MongoChatItem.findOne({
appId,
chatId,
dataId: sharedDataId,
obj: ChatRoleEnum.AI
});
expect(humanChatItem?.isFeedbackRead).toBe(false);
expect(aiChatItem?.isFeedbackRead).toBe(true);
});
it('should not update human chat items without feedback', async () => {
const humanDataId = getNanoid(); const humanDataId = getNanoid();
// Create a human message // Create a human message
...@@ -173,7 +249,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -173,7 +249,7 @@ describe('updateFeedbackReadStatus api test', () => {
const res = await Call< const res = await Call<
UpdateFeedbackReadStatusBodyType, UpdateFeedbackReadStatusBodyType,
{}, Record<string, never>,
UpdateFeedbackReadStatusResponseType UpdateFeedbackReadStatusResponseType
>(handler, { >(handler, {
auth: testUser, auth: testUser,
...@@ -200,7 +276,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -200,7 +276,7 @@ describe('updateFeedbackReadStatus api test', () => {
it('should handle non-existent dataId gracefully', async () => { it('should handle non-existent dataId gracefully', async () => {
const res = await Call< const res = await Call<
UpdateFeedbackReadStatusBodyType, UpdateFeedbackReadStatusBodyType,
{}, Record<string, never>,
UpdateFeedbackReadStatusResponseType UpdateFeedbackReadStatusResponseType
>(handler, { >(handler, {
auth: testUser, auth: testUser,
......
...@@ -79,9 +79,11 @@ describe('updateUserFeedback api test', () => { ...@@ -79,9 +79,11 @@ describe('updateUserFeedback api test', () => {
}); });
it('should add good feedback', async () => { it('should add good feedback', async () => {
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -89,8 +91,7 @@ describe('updateUserFeedback api test', () => { ...@@ -89,8 +91,7 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userGoodFeedback: 'Great answer!' userGoodFeedback: 'Great answer!'
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -116,9 +117,11 @@ describe('updateUserFeedback api test', () => { ...@@ -116,9 +117,11 @@ describe('updateUserFeedback api test', () => {
}); });
it('should add bad feedback', async () => { it('should add bad feedback', async () => {
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -126,8 +129,7 @@ describe('updateUserFeedback api test', () => { ...@@ -126,8 +129,7 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userBadFeedback: 'Not helpful' userBadFeedback: 'Not helpful'
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -160,9 +162,11 @@ describe('updateUserFeedback api test', () => { ...@@ -160,9 +162,11 @@ describe('updateUserFeedback api test', () => {
{ goodFeedbackCount: 1 } { goodFeedbackCount: 1 }
); );
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -170,8 +174,7 @@ describe('updateUserFeedback api test', () => { ...@@ -170,8 +174,7 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userGoodFeedback: undefined userGoodFeedback: undefined
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -203,9 +206,11 @@ describe('updateUserFeedback api test', () => { ...@@ -203,9 +206,11 @@ describe('updateUserFeedback api test', () => {
{ badFeedbackCount: 1 } { badFeedbackCount: 1 }
); );
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -213,8 +218,7 @@ describe('updateUserFeedback api test', () => { ...@@ -213,8 +218,7 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userBadFeedback: undefined userBadFeedback: undefined
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -246,9 +250,11 @@ describe('updateUserFeedback api test', () => { ...@@ -246,9 +250,11 @@ describe('updateUserFeedback api test', () => {
{ goodFeedbackCount: 1 } { goodFeedbackCount: 1 }
); );
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -256,8 +262,7 @@ describe('updateUserFeedback api test', () => { ...@@ -256,8 +262,7 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userGoodFeedback: 'Excellent!' userGoodFeedback: 'Excellent!'
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -281,6 +286,52 @@ describe('updateUserFeedback api test', () => { ...@@ -281,6 +286,52 @@ describe('updateUserFeedback api test', () => {
expect(chatLog?.goodFeedbackCount).toBe(1); expect(chatLog?.goodFeedbackCount).toBe(1);
}); });
it('should update AI feedback when human and AI share the same dataId', async () => {
await MongoChatItem.create({
teamId: testUser.teamId,
tmbId: testUser.tmbId,
userId: testUser.userId,
appId,
chatId,
dataId,
obj: ChatRoleEnum.Human,
value: [{ type: 'text', text: { content: 'Test question' } }]
});
const res = await Call<
UpdateUserFeedbackBodyType,
Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser,
body: {
appId,
chatId,
dataId,
userBadFeedback: 'Not helpful'
}
});
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
const humanChatItem = await MongoChatItem.findOne({
appId,
chatId,
dataId,
obj: ChatRoleEnum.Human
});
const aiChatItem = await MongoChatItem.findOne({
appId,
chatId,
dataId,
obj: ChatRoleEnum.AI
});
expect(humanChatItem?.userBadFeedback).toBeUndefined();
expect(aiChatItem?.userBadFeedback).toBe('Not helpful');
});
it('should switch from good to bad feedback', async () => { it('should switch from good to bad feedback', async () => {
// First add good feedback // First add good feedback
await MongoChatItem.updateOne({ appId, chatId, dataId }, { userGoodFeedback: 'Good' }); await MongoChatItem.updateOne({ appId, chatId, dataId }, { userGoodFeedback: 'Good' });
...@@ -290,9 +341,11 @@ describe('updateUserFeedback api test', () => { ...@@ -290,9 +341,11 @@ describe('updateUserFeedback api test', () => {
); );
// Remove good and add bad feedback // Remove good and add bad feedback
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -301,8 +354,7 @@ describe('updateUserFeedback api test', () => { ...@@ -301,8 +354,7 @@ describe('updateUserFeedback api test', () => {
userGoodFeedback: undefined, userGoodFeedback: undefined,
userBadFeedback: 'Actually not good' userBadFeedback: 'Actually not good'
} }
} });
);
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -329,9 +381,11 @@ describe('updateUserFeedback api test', () => { ...@@ -329,9 +381,11 @@ describe('updateUserFeedback api test', () => {
}); });
it('should fail when chatId is empty', async () => { it('should fail when chatId is empty', async () => {
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -339,17 +393,18 @@ describe('updateUserFeedback api test', () => { ...@@ -339,17 +393,18 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userGoodFeedback: 'Great!' userGoodFeedback: 'Great!'
} }
} });
);
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
}); });
it('should fail when dataId is empty', async () => { it('should fail when dataId is empty', async () => {
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -357,17 +412,18 @@ describe('updateUserFeedback api test', () => { ...@@ -357,17 +412,18 @@ describe('updateUserFeedback api test', () => {
dataId: '', dataId: '',
userGoodFeedback: 'Great!' userGoodFeedback: 'Great!'
} }
} });
);
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
}); });
it('should fail when chat item does not exist', async () => { it('should fail when chat item does not exist', async () => {
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: testUser, auth: testUser,
body: { body: {
appId, appId,
...@@ -375,8 +431,7 @@ describe('updateUserFeedback api test', () => { ...@@ -375,8 +431,7 @@ describe('updateUserFeedback api test', () => {
dataId: 'non-existent-id', dataId: 'non-existent-id',
userGoodFeedback: 'Great!' userGoodFeedback: 'Great!'
} }
} });
);
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
...@@ -385,9 +440,11 @@ describe('updateUserFeedback api test', () => { ...@@ -385,9 +440,11 @@ describe('updateUserFeedback api test', () => {
it('should fail when user does not have permission', async () => { it('should fail when user does not have permission', async () => {
const unauthorizedUser = await getUser(`unauthorized-user-feedback-${Math.random()}`); const unauthorizedUser = await getUser(`unauthorized-user-feedback-${Math.random()}`);
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
UpdateUserFeedbackResponseType
>(handler, {
auth: unauthorizedUser, auth: unauthorizedUser,
body: { body: {
appId, appId,
...@@ -395,8 +452,7 @@ describe('updateUserFeedback api test', () => { ...@@ -395,8 +452,7 @@ describe('updateUserFeedback api test', () => {
dataId, dataId,
userGoodFeedback: 'Great!' userGoodFeedback: 'Great!'
} }
} });
);
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment