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) => (
......
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,18 +125,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -125,18 +125,19 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
feedbackType: 'good', appId,
unreadOnly: false chatId,
} feedbackType: 'good',
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,18 +147,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -146,18 +147,19 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
feedbackType: 'good', appId,
unreadOnly: true chatId,
} feedbackType: 'good',
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,18 +168,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -166,18 +168,19 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
feedbackType: 'bad', appId,
unreadOnly: false chatId,
} feedbackType: 'bad',
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,18 +190,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -187,18 +190,19 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
feedbackType: 'bad', appId,
unreadOnly: true chatId,
} feedbackType: 'bad',
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,18 +211,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -207,18 +211,19 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
feedbackType: 'has_feedback', appId,
unreadOnly: false chatId,
} feedbackType: 'has_feedback',
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,21 +232,23 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -227,21 +232,23 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
feedbackType: 'has_feedback', appId,
unreadOnly: true chatId,
} feedbackType: 'has_feedback',
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,18 +258,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -251,18 +258,19 @@ 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>,
auth: testUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId: '', auth: testUser,
chatId: '', body: {
feedbackType: 'good', appId: '',
unreadOnly: false chatId: '',
} feedbackType: 'good',
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,18 +280,19 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -272,18 +280,19 @@ 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>,
auth: unauthorizedUser, GetFeedbackRecordIdsResponseType
body: { >(handler, {
appId, auth: unauthorizedUser,
chatId, body: {
feedbackType: 'good', appId,
unreadOnly: false chatId,
} feedbackType: 'good',
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,18 +79,19 @@ describe('updateUserFeedback api test', () => { ...@@ -79,18 +79,19 @@ 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>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
userGoodFeedback: 'Great answer!' chatId,
} dataId,
userGoodFeedback: 'Great answer!'
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -116,18 +117,19 @@ describe('updateUserFeedback api test', () => { ...@@ -116,18 +117,19 @@ 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>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
userBadFeedback: 'Not helpful' chatId,
} dataId,
userBadFeedback: 'Not helpful'
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -160,18 +162,19 @@ describe('updateUserFeedback api test', () => { ...@@ -160,18 +162,19 @@ describe('updateUserFeedback api test', () => {
{ goodFeedbackCount: 1 } { goodFeedbackCount: 1 }
); );
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
userGoodFeedback: undefined chatId,
} dataId,
userGoodFeedback: undefined
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -203,18 +206,19 @@ describe('updateUserFeedback api test', () => { ...@@ -203,18 +206,19 @@ describe('updateUserFeedback api test', () => {
{ badFeedbackCount: 1 } { badFeedbackCount: 1 }
); );
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
userBadFeedback: undefined chatId,
} dataId,
userBadFeedback: undefined
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -246,18 +250,19 @@ describe('updateUserFeedback api test', () => { ...@@ -246,18 +250,19 @@ describe('updateUserFeedback api test', () => {
{ goodFeedbackCount: 1 } { goodFeedbackCount: 1 }
); );
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>( const res = await Call<
handler, UpdateUserFeedbackBodyType,
{ Record<string, never>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
userGoodFeedback: 'Excellent!' chatId,
} dataId,
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,19 +341,20 @@ describe('updateUserFeedback api test', () => { ...@@ -290,19 +341,20 @@ 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>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
userGoodFeedback: undefined, chatId,
userBadFeedback: 'Actually not good' dataId,
} userGoodFeedback: undefined,
userBadFeedback: 'Actually not good'
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -329,54 +381,57 @@ describe('updateUserFeedback api test', () => { ...@@ -329,54 +381,57 @@ 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>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId: '', body: {
dataId, appId,
userGoodFeedback: 'Great!' chatId: '',
} dataId,
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>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId: '', appId,
userGoodFeedback: 'Great!' chatId,
} dataId: '',
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>,
auth: testUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId: 'non-existent-id', appId,
userGoodFeedback: 'Great!' chatId,
} dataId: 'non-existent-id',
userGoodFeedback: 'Great!'
} }
); });
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
...@@ -385,18 +440,19 @@ describe('updateUserFeedback api test', () => { ...@@ -385,18 +440,19 @@ 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>,
auth: unauthorizedUser, UpdateUserFeedbackResponseType
body: { >(handler, {
appId, auth: unauthorizedUser,
chatId, body: {
dataId, appId,
userGoodFeedback: 'Great!' chatId,
} dataId,
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