Commit f9ba442c by Xianquan Committed by GitHub

fix: polish chat UI interactions (#7076)

* fix: polish chat UI interactions

* fix: resolve chat UI build errors

---------

Co-authored-by: archer <545436317@qq.com>
parent 14228b00
...@@ -230,8 +230,8 @@ export const MultipleRowSelect = ({ ...@@ -230,8 +230,8 @@ export const MultipleRowSelect = ({
} }
: {})} : {})}
> >
<Flex alignItems={'center'}> <Flex alignItems={'center'} minW={0} overflow={'hidden'}>
<Box flex="1" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap"> <Box flex="1 1 0" minW={0} overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
{label ?? placeholder} {label ?? placeholder}
</Box> </Box>
<MyIcon name={'core/chat/chevronDown'} w={4} flexShrink={0} color={'myGray.500'} /> <MyIcon name={'core/chat/chevronDown'} w={4} flexShrink={0} color={'myGray.500'} />
......
...@@ -243,8 +243,8 @@ const MySelect = <T = any,>( ...@@ -243,8 +243,8 @@ const MySelect = <T = any,>(
_hover={isInvalid ? { borderColor: 'red.400' } : { borderColor: 'primary.300' }} _hover={isInvalid ? { borderColor: 'red.400' } : { borderColor: 'primary.300' }}
{...props} {...props}
> >
<Flex alignItems={'center'} justifyContent="space-between" w="100%"> <Flex alignItems={'center'} justifyContent="space-between" w="100%" minW={0}>
<Flex alignItems={'center'}> <Flex alignItems={'center'} flex={'1 1 0'} minW={0} overflow={'hidden'}>
{isSelecting && <MyIcon mr={2} name={'common/loading'} w={'1rem'} />} {isSelecting && <MyIcon mr={2} name={'common/loading'} w={'1rem'} />}
{valueLabel ? ( {valueLabel ? (
<>{valueLabel}</> <>{valueLabel}</>
......
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
"completion_finish_reason": "Reason for completion", "completion_finish_reason": "Reason for completion",
"completion_finish_stop": "Completed normally", "completion_finish_stop": "Completed normally",
"completion_finish_tool_calls": "Tool calls", "completion_finish_tool_calls": "Tool calls",
"feedback_helpful": "Helpful",
"feedback_unhelpful": "Not helpful",
"compress_llm_messages": "Context compression", "compress_llm_messages": "Context compression",
"compress_llm_usage": "Text compression input and output tokens", "compress_llm_usage": "Text compression input and output tokens",
"compress_llm_usage_point": "Text compression points consumption", "compress_llm_usage_point": "Text compression points consumption",
......
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
"completion_finish_reason": "完成原因", "completion_finish_reason": "完成原因",
"completion_finish_stop": "正常完成", "completion_finish_stop": "正常完成",
"completion_finish_tool_calls": "工具调用", "completion_finish_tool_calls": "工具调用",
"feedback_helpful": "有帮助",
"feedback_unhelpful": "没有帮助",
"compress_llm_messages": "上下文压缩", "compress_llm_messages": "上下文压缩",
"compress_llm_usage": "文本压缩输入输出 tokens", "compress_llm_usage": "文本压缩输入输出 tokens",
"compress_llm_usage_point": "文本压缩积分消耗", "compress_llm_usage_point": "文本压缩积分消耗",
...@@ -74,7 +76,7 @@ ...@@ -74,7 +76,7 @@
"input_guide": "输入引导", "input_guide": "输入引导",
"input_guide_lexicon": "词库", "input_guide_lexicon": "词库",
"input_guide_tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。", "input_guide_tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。",
"input_placeholder_phone": "输入问题", "input_placeholder_phone": "发消息给FastGPT",
"insert_input_guide,_some_data_already_exists": "有重复数据,已自动过滤,共插入 {{len}} 条数据", "insert_input_guide,_some_data_already_exists": "有重复数据,已自动过滤,共插入 {{len}} 条数据",
"internal_variables_tip": "内部变量仅调试时可见", "internal_variables_tip": "内部变量仅调试时可见",
"invalid_share_url": "无效的分享链接", "invalid_share_url": "无效的分享链接",
......
...@@ -374,7 +374,7 @@ ...@@ -374,7 +374,7 @@
"core.chat.Select dataset Desc": "选择一个知识库存储预期答案", "core.chat.Select dataset Desc": "选择一个知识库存储预期答案",
"core.chat.Send Message": "发送", "core.chat.Send Message": "发送",
"core.chat.Speaking": "我在听,请说...", "core.chat.Speaking": "我在听,请说...",
"core.chat.Type a message": "输入问题,发送 [Enter]/换行 [Ctrl(Alt/Shift) + Enter]", "core.chat.Type a message": "发消息给FastGPT",
"core.chat.Unpin": "取消置顶", "core.chat.Unpin": "取消置顶",
"core.chat.error.Chat error": "对话出现异常", "core.chat.error.Chat error": "对话出现异常",
"core.chat.error.Messages empty": "接口内容为空,可能文本超长了~", "core.chat.error.Messages empty": "接口内容为空,可能文本超长了~",
......
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
"completion_finish_reason": "完成原因", "completion_finish_reason": "完成原因",
"completion_finish_stop": "正常完成", "completion_finish_stop": "正常完成",
"completion_finish_tool_calls": "工具呼叫", "completion_finish_tool_calls": "工具呼叫",
"feedback_helpful": "有幫助",
"feedback_unhelpful": "沒有幫助",
"compress_llm_messages": "上下文壓縮", "compress_llm_messages": "上下文壓縮",
"compress_llm_usage": "文本壓縮輸入輸出 tokens", "compress_llm_usage": "文本壓縮輸入輸出 tokens",
"compress_llm_usage_point": "文本壓縮積分消耗", "compress_llm_usage_point": "文本壓縮積分消耗",
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
display: inline-block; display: inline-block;
content: ''; content: '';
width: 3px; width: 3px;
max-width: 3px;
height: 14px; height: 14px;
transform: translate(4px, 2px) scaleY(1.3); transform: translate(4px, 2px) scaleY(1.3);
background-color: var(--chakra-colors-primary-700); background-color: var(--chakra-colors-primary-700);
animation: blink 0.6s infinite; animation: blink 0.6s infinite;
vertical-align: baseline;
} }
.animation { .animation {
...@@ -45,8 +47,8 @@ ...@@ -45,8 +47,8 @@
cursor: pointer; cursor: pointer;
display: block; display: block;
left: 0; left: 0;
margin-left: -30px; margin-left: 0;
padding-left: 30px; padding-left: 0;
position: absolute; position: absolute;
top: 0; top: 0;
} }
...@@ -61,6 +63,9 @@ ...@@ -61,6 +63,9 @@
margin: 10px 0; margin: 10px 0;
padding: 0; padding: 0;
position: relative; position: relative;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
} }
.markdown h1 .mini-icon-link, .markdown h1 .mini-icon-link,
.markdown h2 .mini-icon-link, .markdown h2 .mini-icon-link,
...@@ -77,7 +82,7 @@ ...@@ -77,7 +82,7 @@
.markdown h5:hover a.anchor, .markdown h5:hover a.anchor,
.markdown h6:hover a.anchor { .markdown h6:hover a.anchor {
line-height: 1; line-height: 1;
margin-left: -22px; margin-left: 0;
padding-left: 0; padding-left: 0;
text-decoration: none; text-decoration: none;
top: 15%; top: 15%;
...@@ -163,7 +168,8 @@ ...@@ -163,7 +168,8 @@
} }
.markdown ul, .markdown ul,
.markdown ol { .markdown ol {
padding-left: 2em; padding-left: 0;
list-style-position: inside;
} }
.markdown ul.no-list, .markdown ul.no-list,
.markdown ol.no-list { .markdown ol.no-list {
...@@ -176,7 +182,8 @@ ...@@ -176,7 +182,8 @@
} }
.markdown ul, .markdown ul,
.markdown ol { .markdown ol {
padding-left: 14px; padding-left: 0;
list-style-position: inside;
} }
.markdown dl { .markdown dl {
padding: 0; padding: 0;
......
...@@ -300,7 +300,7 @@ const ChatInput = ({ ...@@ -300,7 +300,7 @@ const ChatInput = ({
gap={[0, 1]} gap={[0, 1]}
> >
{/* 左侧自定义按钮组 */} {/* 左侧自定义按钮组 */}
<Flex alignItems={'center'} gap={2} flex={'1 0 0'} w={0}> <Flex alignItems={'center'} gap={2} flex={'1 1 0'} minW={0} w={0}>
{InputLeftComponent} {InputLeftComponent}
</Flex> </Flex>
...@@ -523,7 +523,7 @@ const ChatInput = ({ ...@@ -523,7 +523,7 @@ const ChatInput = ({
{!mobilePreSpeak && <Box>{RenderButtonGroup}</Box>} {!mobilePreSpeak && <Box>{RenderButtonGroup}</Box>}
</Flex> </Flex>
<ComplianceTip type={'chat'} pt={4} pb={0} /> <ComplianceTip type={'chat'} pt={4} pb={['calc(12px + env(safe-area-inset-bottom))', 3]} />
</Box> </Box>
); );
}; };
......
...@@ -2,6 +2,7 @@ import { Box, Flex } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React from 'react'; import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import ChatController, { type ChatControllerProps } from '../ChatController'; import ChatController, { type ChatControllerProps } from '../ChatController';
import { ChatBoxContext } from '../../Provider'; import { ChatBoxContext } from '../../Provider';
...@@ -44,15 +45,17 @@ const AIChatBubbleActions = ({ ...@@ -44,15 +45,17 @@ const AIChatBubbleActions = ({
<ChatController {...chatControllerProps} variant="footer" /> <ChatController {...chatControllerProps} variant="footer" />
{showRetry && ( {showRetry && (
<MyIcon <MyTooltip label={t('common:core.chat.retry')}>
name={'common/retryLight'} <MyIcon
w={'16px'} name={'common/retryLight'}
p={'4px'} w={'16px'}
cursor={'pointer'} p={'4px'}
color={'myGray.400'} cursor={'pointer'}
_hover={{ color: 'primary.600' }} color={'myGray.400'}
onClick={onRetry} _hover={{ color: 'primary.600' }}
/> onClick={onRetry}
/>
</MyTooltip>
)} )}
{showWholeResponse && ( {showWholeResponse && (
......
...@@ -7,10 +7,15 @@ import type { ...@@ -7,10 +7,15 @@ import type {
import type { OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext'; import type { OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext';
import AIResponseBox from '../../../../components/AIResponseBox'; import AIResponseBox from '../../../../components/AIResponseBox';
import RenderProcessingCollapse from '../../../../components/AIResponseBox/RenderProcessingCollapse'; import RenderProcessingCollapse from '../../../../components/AIResponseBox/RenderProcessingCollapse';
import RenderProcessingPreview, {
getProcessingPreviewLabelKey
} from '../../../../components/AIResponseBox/RenderProcessingPreview';
import { import {
hasAiAnswerContent, hasAiAnswerContent,
hasAiFoldableProcessingContent,
hasAiInteractiveContent, hasAiInteractiveContent,
hasAiProcessingContent hasAiProcessingContent,
hasAiStandaloneProcessingContent
} from './utils'; } from './utils';
type AIChatBubbleContentProps = { type AIChatBubbleContentProps = {
...@@ -35,15 +40,21 @@ const AIChatBubbleContent = ({ ...@@ -35,15 +40,21 @@ const AIChatBubbleContent = ({
index, index,
wrapProcessing, wrapProcessing,
showProcessing = true, showProcessing = true,
showFoldableProcessing,
showStandaloneProcessing,
showAnswer = true, showAnswer = true,
showInteractive = true showInteractive = true,
defaultExpandProcessing = true
}: { }: {
value: AIChatItemValueItemType; value: AIChatItemValueItemType;
index: number; index: number;
wrapProcessing: boolean; wrapProcessing: boolean;
showProcessing?: boolean; showProcessing?: boolean;
showFoldableProcessing?: boolean;
showStandaloneProcessing?: boolean;
showAnswer?: boolean; showAnswer?: boolean;
showInteractive?: boolean; showInteractive?: boolean;
defaultExpandProcessing?: boolean;
}) => { }) => {
const isLastResponse = isLastChild && index === chatValue.length - 1; const isLastResponse = isLastChild && index === chatValue.length - 1;
...@@ -58,8 +69,11 @@ const AIChatBubbleContent = ({ ...@@ -58,8 +69,11 @@ const AIChatBubbleContent = ({
onOpenCiteModal={onOpenCiteModal} onOpenCiteModal={onOpenCiteModal}
wrapProcessing={wrapProcessing} wrapProcessing={wrapProcessing}
showProcessing={showProcessing} showProcessing={showProcessing}
showFoldableProcessing={showFoldableProcessing}
showStandaloneProcessing={showStandaloneProcessing}
showAnswer={showAnswer} showAnswer={showAnswer}
showInteractive={showInteractive} showInteractive={showInteractive}
defaultExpandProcessing={defaultExpandProcessing}
/> />
); );
}; };
...@@ -67,39 +81,41 @@ const AIChatBubbleContent = ({ ...@@ -67,39 +81,41 @@ const AIChatBubbleContent = ({
const contentBlocks: React.ReactNode[] = []; const contentBlocks: React.ReactNode[] = [];
let processingGroup: Array<{ value: AIChatItemValueItemType; index: number }> = []; let processingGroup: Array<{ value: AIChatItemValueItemType; index: number }> = [];
const flushProcessingGroup = (isProcessing: boolean) => { const flushProcessingGroup = () => {
if (processingGroup.length === 0) return; if (processingGroup.length === 0) return;
const group = processingGroup; const group = processingGroup;
processingGroup = []; processingGroup = [];
const previewItem = group[group.length - 1];
if (group.length === 1) { const isProcessing =
const { value, index } = group[0]; isChatting && isLastChild && group.some(({ index }) => index === chatValue.length - 1);
contentBlocks.push(
<Box key={`${dataId}-ai-${index}`} _notFirst={{ mt: 4 }}>
{renderValue({
value,
index,
wrapProcessing: false,
showAnswer: false,
showInteractive: false
})}
</Box>
);
return;
}
contentBlocks.push( contentBlocks.push(
<Box key={`${dataId}-processing-${group[0].index}`} _notFirst={{ mt: 4 }}> <Box key={`${dataId}-processing-${group[0].index}`} _notFirst={{ mt: 2 }}>
<RenderProcessingCollapse isProcessing={isProcessing}> <RenderProcessingCollapse
isProcessing={isProcessing}
label={previewItem ? getProcessingPreviewLabelKey(previewItem.value) : undefined}
preview={
previewItem
? (
<RenderProcessingPreview
value={previewItem.value}
showAnimation={isProcessing}
/>
)
: undefined
}
>
{group.map(({ value, index }) => ( {group.map(({ value, index }) => (
<Box key={`${dataId}-ai-${index}`} _notFirst={{ mt: 4 }}> <Box key={`${dataId}-ai-${index}`}>
{renderValue({ {renderValue({
value, value,
index, index,
wrapProcessing: false, wrapProcessing: false,
showAnswer: false, showAnswer: false,
showInteractive: false showInteractive: false,
showStandaloneProcessing: false,
defaultExpandProcessing: false
})} })}
</Box> </Box>
))} ))}
...@@ -112,29 +128,37 @@ const AIChatBubbleContent = ({ ...@@ -112,29 +128,37 @@ const AIChatBubbleContent = ({
if (value.hideInUI) return; if (value.hideInUI) return;
const hasProcessing = hasAiProcessingContent(value); const hasProcessing = hasAiProcessingContent(value);
const hasFoldableProcessing = hasAiFoldableProcessingContent(value);
const hasStandaloneProcessing = hasAiStandaloneProcessingContent(value);
const hasAnswer = hasAiAnswerContent(value); const hasAnswer = hasAiAnswerContent(value);
const hasInteractive = hasAiInteractiveContent(value); const hasInteractive = hasAiInteractiveContent(value);
if (!hasProcessing && !hasAnswer && !hasInteractive) return; if (!hasProcessing && !hasAnswer && !hasInteractive) return;
if (hasProcessing) { if (hasFoldableProcessing) {
processingGroup.push({ value, index }); processingGroup.push({ value, index });
if (!hasAnswer && !hasInteractive) { if (!hasStandaloneProcessing && !hasAnswer && !hasInteractive) {
return; return;
} }
} }
flushProcessingGroup(isChatting && isLastChild && !hasAnswer && !hasInteractive); flushProcessingGroup();
contentBlocks.push( contentBlocks.push(
<Box key={`${dataId}-ai-${index}`} _notFirst={{ mt: 4 }}> <Box key={`${dataId}-ai-${index}`} _notFirst={{ mt: 2 }}>
{renderValue({ value, index, wrapProcessing: true, showProcessing: false })} {renderValue({
value,
index,
wrapProcessing: true,
showFoldableProcessing: false,
showStandaloneProcessing: hasStandaloneProcessing
})}
</Box> </Box>
); );
}); });
flushProcessingGroup(isChatting && isLastChild); flushProcessingGroup();
return ( return (
<Flex flexDirection={'column'} fontSize={'16px'} lineHeight={1.75}> <Flex flexDirection={'column'} fontSize={'16px'} lineHeight={1.75}>
......
...@@ -11,6 +11,7 @@ export const hasAiProcessingContent = (item: AIChatItemValueItemType) => { ...@@ -11,6 +11,7 @@ export const hasAiProcessingContent = (item: AIChatItemValueItemType) => {
const tools = item.tools || (item.tool ? [item.tool] : undefined); const tools = item.tools || (item.tool ? [item.tool] : undefined);
return Boolean( return Boolean(
(item.reasoning?.content && !item.hideReason) || (item.reasoning?.content && !item.hideReason) ||
(item.agentPlanUpdate?.reasoningText && !item.hideReason) ||
tools?.length || tools?.length ||
item.skills?.length || item.skills?.length ||
item.plan || item.plan ||
...@@ -18,6 +19,25 @@ export const hasAiProcessingContent = (item: AIChatItemValueItemType) => { ...@@ -18,6 +19,25 @@ export const hasAiProcessingContent = (item: AIChatItemValueItemType) => {
); );
}; };
export const hasAiFoldableProcessingContent = (item: AIChatItemValueItemType) => {
if (item.hideInUI) return false;
const tools = item.tools || (item.tool ? [item.tool] : undefined);
return Boolean(
(item.reasoning?.content && !item.hideReason) ||
(item.agentPlanUpdate?.reasoningText && !item.hideReason) ||
tools?.length
);
};
export const hasAiStandaloneProcessingContent = (item: AIChatItemValueItemType) => {
if (item.hideInUI) return false;
return Boolean(
item.skills?.length || item.plan || item.planStatus?.status === 'generating'
);
};
export const shouldFilterAiValue = (item: AIChatItemValueItemType) => { export const shouldFilterAiValue = (item: AIChatItemValueItemType) => {
if (item.hideInUI) return true; if (item.hideInUI) return true;
if (item.text?.content?.trim() || item.reasoning?.content?.trim()) return false; if (item.text?.content?.trim() || item.reasoning?.content?.trim()) return false;
......
...@@ -82,8 +82,9 @@ const ChatController = ({ ...@@ -82,8 +82,9 @@ const ChatController = ({
const isLogMode = chatType === 'log'; const isLogMode = chatType === 'log';
const isFooter = variant === 'footer'; const isFooter = variant === 'footer';
const renderTooltip = (label: string, children: React.ReactNode) => const renderTooltip = (label: string, children: React.ReactNode) => (
isFooter ? <>{children}</> : <MyTooltip label={label}>{children}</MyTooltip>; <MyTooltip label={label}>{children}</MyTooltip>
);
const iconStyle = isFooter ? footerIconStyle : controlIconStyle; const iconStyle = isFooter ? footerIconStyle : controlIconStyle;
const activeFeedbackStyle = isFooter const activeFeedbackStyle = isFooter
? { ? {
...@@ -243,80 +244,90 @@ const ChatController = ({ ...@@ -243,80 +244,90 @@ const ChatController = ({
{isLogMode ? ( {isLogMode ? (
<> <>
{!!chat.userGoodFeedback && ( {!!chat.userGoodFeedback && (
<Box position={'relative'}> <MyTooltip label={t('chat:feedback_helpful')}>
<MyIcon <Box position={'relative'}>
{...iconStyle} <MyIcon
color={'green.500'} {...iconStyle}
name={'core/chat/feedback/goodLight'} color={'green.500'}
cursor={'not-allowed'} name={'core/chat/feedback/goodLight'}
/> cursor={'not-allowed'}
{!chat.isFeedbackRead && (
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/> />
)} {!chat.isFeedbackRead && (
</Box> <Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
)}
</Box>
</MyTooltip>
)} )}
{!!chat.userBadFeedback && ( {!!chat.userBadFeedback && (
<Box position={'relative'}> <MyTooltip label={t('chat:feedback_unhelpful')}>
<MyIcon <Box position={'relative'}>
{...iconStyle} <MyIcon
color={'yellow.500'} {...iconStyle}
name={'core/chat/feedback/badLight'} color={'yellow.500'}
cursor={'not-allowed'} name={'core/chat/feedback/badLight'}
/> cursor={'not-allowed'}
{!chat.isFeedbackRead && (
<Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/> />
)} {!chat.isFeedbackRead && (
</Box> <Box
position={'absolute'}
top={'-2px'}
right={'-2px'}
w={'8px'}
h={'8px'}
bg={'red.500'}
borderRadius={'full'}
border={'1px solid white'}
/>
)}
</Box>
</MyTooltip>
)} )}
</> </>
) : ( ) : (
<> <>
{!!onAddUserLike && ( {!!onAddUserLike && (
<MyIcon <MyTooltip label={t('chat:feedback_helpful')}>
{...iconStyle} <MyIcon
{...(!!chat.userGoodFeedback {...iconStyle}
? activeFeedbackStyle {...(!!chat.userGoodFeedback
: { ? activeFeedbackStyle
_hover: { color: 'primary.600' } : {
})} _hover: { color: 'primary.600' }
borderRight={isFooter ? undefined : !onAddUserDislike ? 'none' : 'base'} })}
borderRightRadius={isFooter ? undefined : !onAddUserDislike ? 'sm' : 'none'} borderRight={isFooter ? undefined : !onAddUserDislike ? 'none' : 'base'}
name={'core/chat/feedback/goodLight'} borderRightRadius={
onClick={onAddUserLike} isFooter ? undefined : !onAddUserDislike ? 'sm' : 'none'
/> }
name={'core/chat/feedback/goodLight'}
onClick={onAddUserLike}
/>
</MyTooltip>
)} )}
{!!onAddUserDislike && ( {!!onAddUserDislike && (
<MyIcon <MyTooltip label={t('chat:feedback_unhelpful')}>
{...iconStyle} <MyIcon
{...(!!chat.userBadFeedback {...iconStyle}
? activeBadFeedbackStyle {...(!!chat.userBadFeedback
: { ? activeBadFeedbackStyle
_hover: { color: 'primary.600' } : {
})} _hover: { color: 'primary.600' }
borderRight={isFooter ? undefined : 'none'} })}
borderRightRadius={isFooter ? undefined : 'sm'} borderRight={isFooter ? undefined : 'none'}
name={'core/chat/feedback/badLight'} borderRightRadius={isFooter ? undefined : 'sm'}
onClick={onAddUserDislike} name={'core/chat/feedback/badLight'}
/> onClick={onAddUserDislike}
/>
</MyTooltip>
)} )}
</> </>
)} )}
......
...@@ -178,10 +178,10 @@ const ChatRecordsList = ({ ...@@ -178,10 +178,10 @@ const ChatRecordsList = ({
> >
<Box <Box
pt={ pt={
item.hideInUI ? 0 : item.obj === ChatRoleEnum.Human ? 0 : '20px' item.hideInUI ? 0 : item.obj === ChatRoleEnum.Human ? 0 : '8px'
} }
pb={ pb={
item.hideInUI ? 0 : item.obj === ChatRoleEnum.Human ? '20px' : '40px' item.hideInUI ? 0 : item.obj === ChatRoleEnum.Human ? '4px' : '32px'
} }
> >
{item.obj === ChatRoleEnum.Human && !item.hideInUI && ( {item.obj === ChatRoleEnum.Human && !item.hideInUI && (
......
...@@ -38,7 +38,9 @@ const ChatVariableCard = ({ children }: { children: React.ReactNode }) => { ...@@ -38,7 +38,9 @@ const ChatVariableCard = ({ children }: { children: React.ReactNode }) => {
boxShadow={'0 4px 10px 0 rgba(19, 51, 107, 0.08), 0 0 1px 0 rgba(19, 51, 107, 0.08)'} boxShadow={'0 4px 10px 0 rgba(19, 51, 107, 0.08), 0 0 1px 0 rgba(19, 51, 107, 0.08)'}
p={4} p={4}
> >
{children} <Flex flexDirection={'column'} gap={4}>
{children}
</Flex>
</Card> </Card>
); );
}; };
...@@ -48,7 +50,6 @@ const ChatVariableTip = ({ children }: { children: React.ReactNode }) => { ...@@ -48,7 +50,6 @@ const ChatVariableTip = ({ children }: { children: React.ReactNode }) => {
<Flex <Flex
color={'primary.600'} color={'primary.600'}
bg={'primary.50'} bg={'primary.50'}
mb={4}
px={3} px={3}
py={2} py={2}
gap={2} gap={2}
...@@ -109,7 +110,15 @@ export const ChatVariableFields = ({ ...@@ -109,7 +110,15 @@ export const ChatVariableFields = ({
isUnChange: boolean; isUnChange: boolean;
}) => { }) => {
return ( return (
<> <Flex
flexDirection={'column'}
gap={4}
sx={{
'& > *': {
mb: '0 !important'
}
}}
>
{variables.map((item) => ( {variables.map((item) => (
<LabelAndFormRender <LabelAndFormRender
{...item} {...item}
...@@ -122,7 +131,7 @@ export const ChatVariableFields = ({ ...@@ -122,7 +131,7 @@ export const ChatVariableFields = ({
bg={'white'} bg={'white'}
/> />
))} ))}
</> </Flex>
); );
}; };
...@@ -136,7 +145,7 @@ const ChatVariableStartButton = ({ ...@@ -136,7 +145,7 @@ const ChatVariableStartButton = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Flex justifyContent={'flex-end'} mt={6}> <Flex justifyContent={'flex-end'}>
<Button <Button
w={'69px'} w={'69px'}
h={'36px'} h={'36px'}
......
...@@ -2,6 +2,7 @@ import { Box, Flex } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Box, Flex } from '@chakra-ui/react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time'; import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
...@@ -46,24 +47,28 @@ const HumanChatBubbleActions = ({ ...@@ -46,24 +47,28 @@ const HumanChatBubbleActions = ({
</Box> </Box>
)} )}
<Flex alignItems={'center'} gap={1}> <Flex alignItems={'center'} gap={1}>
<MyIcon <MyTooltip label={t('common:Copy')}>
w={'16px'} <MyIcon
p={'4px'} w={'16px'}
cursor="pointer" p={'4px'}
name={'copy'} cursor="pointer"
color={'myGray.400'} name={'copy'}
_hover={{ color: 'primary.600' }} color={'myGray.400'}
onClick={() => copyData(chatText)} _hover={{ color: 'primary.600' }}
/> onClick={() => copyData(chatText)}
<MyIcon />
w={'16px'} </MyTooltip>
p={'4px'} <MyTooltip label={t('common:Edit')}>
cursor="pointer" <MyIcon
name={'edit'} w={'16px'}
color={'myGray.400'} p={'4px'}
_hover={{ color: 'primary.600' }} cursor="pointer"
onClick={onEdit} name={'edit'}
/> color={'myGray.400'}
_hover={{ color: 'primary.600' }}
onClick={onEdit}
/>
</MyTooltip>
</Flex> </Flex>
</Flex> </Flex>
); );
......
...@@ -85,14 +85,7 @@ const HumanChatBubbleEditForm = ({ ...@@ -85,14 +85,7 @@ const HumanChatBubbleEditForm = ({
showSelectVideo || showSelectVideo ||
showSelectAudio || showSelectAudio ||
showSelectCustomFileExtension; showSelectCustomFileExtension;
const defaultFileUrls = new Set(defaultFiles.map((file) => file.url || file.id)); const canSubmit = !hasFileUploading && trimmedValue.length > 0;
const filesChanged =
fileList.length !== defaultFiles.length ||
fileList.some((file) => !defaultFileUrls.has(file.url || file.id));
const canSubmit =
!hasFileUploading &&
trimmedValue.length > 0 &&
(trimmedValue !== defaultValue.trim() || filesChanged);
useRequest(uploadFiles, { useRequest(uploadFiles, {
manual: false, manual: false,
......
...@@ -49,9 +49,10 @@ const HumanChatBubble = ({ chatValue, chatTime, onEditSubmit, children }: HumanC ...@@ -49,9 +49,10 @@ const HumanChatBubble = ({ chatValue, chatTime, onEditSubmit, children }: HumanC
return ( return (
<Box <Box
display={'inline-block'} display={'flex'}
justifyContent={'flex-end'}
position={'relative'} position={'relative'}
w={'fit-content'} w={'100%'}
maxW={'100%'} maxW={'100%'}
_hover={{ _hover={{
'& .chat-controller-hover': { '& .chat-controller-hover': {
...@@ -59,26 +60,28 @@ const HumanChatBubble = ({ chatValue, chatTime, onEditSubmit, children }: HumanC ...@@ -59,26 +60,28 @@ const HumanChatBubble = ({ chatValue, chatTime, onEditSubmit, children }: HumanC
} }
}} }}
> >
<Card <Box position={'relative'} w={'fit-content'} maxW={'100%'}>
{...MessageCardStyle} <Card
bg={'primary.50'} {...MessageCardStyle}
color={'myGray.900'} bg={'primary.50'}
px={4} color={'myGray.900'}
pt={3} px={4}
pb={MessageCardStyle.py} pt={3}
borderRadius={'12px'} pb={MessageCardStyle.py}
textAlign={'left'} borderRadius={'12px'}
maxW={'100%'} textAlign={'left'}
> maxW={'100%'}
<HumanChatBubbleContent chatValue={chatValue} /> >
{children} <HumanChatBubbleContent chatValue={chatValue} />
</Card> {children}
<HumanChatBubbleActions </Card>
chatText={chatText} <HumanChatBubbleActions
chatTime={chatTime} chatText={chatText}
isAlwaysVisible={!isPc} chatTime={chatTime}
onEdit={() => setIsEditing(true)} isAlwaysVisible={!isPc}
/> onEdit={() => setIsEditing(true)}
/>
</Box>
{!isPc && isEditing && ( {!isPc && isEditing && (
<HumanChatBubbleEditDrawer <HumanChatBubbleEditDrawer
isOpen={isEditing} isOpen={isEditing}
......
...@@ -27,14 +27,6 @@ export type CitationRenderItem = { ...@@ -27,14 +27,6 @@ export type CitationRenderItem = {
const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal')); const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
const getCitationGridSpan = (text: string) => {
const textWidthUnits = Array.from(text).reduce((sum, char) => {
return sum + (/[\u4E00-\u9FFF]/.test(char) ? 2 : 1);
}, 0);
return Math.min(Math.max(Math.ceil(textWidthUnits / 7), 1), 5);
};
const CitationListCard = React.memo(function CitationListCard({ const CitationListCard = React.memo(function CitationListCard({
items, items,
isPc, isPc,
...@@ -128,10 +120,9 @@ const CitationListCard = React.memo(function CitationListCard({ ...@@ -128,10 +120,9 @@ const CitationListCard = React.memo(function CitationListCard({
)} )}
</Flex> </Flex>
<Box <Flex
mt={'4px'} mt={'4px'}
display={'grid'} flexWrap={'wrap'}
gridTemplateColumns={'repeat(5, minmax(0, 1fr))'}
gap={'4px'} gap={'4px'}
> >
{items.map((item) => ( {items.map((item) => (
...@@ -140,7 +131,7 @@ const CitationListCard = React.memo(function CitationListCard({ ...@@ -140,7 +131,7 @@ const CitationListCard = React.memo(function CitationListCard({
alignItems={'center'} alignItems={'center'}
minW={0} minW={0}
w={'max-content'} w={'max-content'}
gridColumn={`span ${getCitationGridSpan(item.displayText)}`} maxW={'100%'}
px={'8px'} px={'8px'}
py={'6px'} py={'6px'}
borderRadius={'8px'} borderRadius={'8px'}
...@@ -156,13 +147,13 @@ const CitationListCard = React.memo(function CitationListCard({ ...@@ -156,13 +147,13 @@ const CitationListCard = React.memo(function CitationListCard({
}} }}
> >
<MyIcon name={item.icon as any} mr={2} flexShrink={0} w={'14px'} /> <MyIcon name={item.icon as any} mr={2} flexShrink={0} w={'14px'} />
<Box minW={0} whiteSpace={'nowrap'}> <Box className={'textEllipsis'} minW={0}>
{item.displayText} {item.displayText}
</Box> </Box>
</Flex> </Flex>
</MyTooltip> </MyTooltip>
))} ))}
</Box> </Flex>
</Box> </Box>
{!expanded && isOverflow && ( {!expanded && isOverflow && (
......
...@@ -9,19 +9,28 @@ import { ...@@ -9,19 +9,28 @@ import {
HStack HStack
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React from 'react'; import React, { useState } from 'react';
const RenderProcessingCollapse = React.memo(function RenderProcessingCollapse({ const RenderProcessingCollapse = React.memo(function RenderProcessingCollapse({
children, children,
label,
preview,
isProcessing = true isProcessing = true
}: { }: {
children: React.ReactNode; children: React.ReactNode;
label?: string;
preview?: React.ReactNode;
isProcessing?: boolean; isProcessing?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(false);
return ( return (
<Accordion allowToggle> <Accordion
allowToggle
index={isExpanded ? 0 : -1}
onChange={(index) => setIsExpanded(Array.isArray(index) ? index.length > 0 : index === 0)}
>
<AccordionItem borderTop={'none'} borderBottom={'none'}> <AccordionItem borderTop={'none'} borderBottom={'none'}>
<Box w={'full'} pb={'4px'} borderBottom={'1px solid'} borderBottomColor={'myGray.200'}> <Box w={'full'} pb={'4px'} borderBottom={'1px solid'} borderBottomColor={'myGray.200'}>
<AccordionButton <AccordionButton
...@@ -38,14 +47,22 @@ const RenderProcessingCollapse = React.memo(function RenderProcessingCollapse({ ...@@ -38,14 +47,22 @@ const RenderProcessingCollapse = React.memo(function RenderProcessingCollapse({
<HStack mr={1} spacing="0"> <HStack mr={1} spacing="0">
<Box fontSize={'16px'} lineHeight={'24px'}> <Box fontSize={'16px'} lineHeight={'24px'}>
{t(isProcessing ? 'chat:processing' : 'chat:processed')} {t(isProcessing ? 'chat:processing' : 'chat:processed')}
{isProcessing && label && (
<Box as="span">
{' · '}
{t(label)}
{'...'}
</Box>
)}
</Box> </Box>
</HStack> </HStack>
<AccordionIcon ml={1} color={'myGray.500'} /> <AccordionIcon ml={1} color={'myGray.500'} />
</AccordionButton> </AccordionButton>
</Box> </Box>
<AccordionPanel py={0} pr={0} pl={0} mt={4}> {isProcessing && !isExpanded && preview && <Box mt={2}>{preview}</Box>}
<Flex flexDirection={'column'} gap={4}> <AccordionPanel py={0} pr={0} pl={0} mt={2}>
<Flex flexDirection={'column'} gap={2}>
{children} {children}
</Flex> </Flex>
</AccordionPanel> </AccordionPanel>
......
import Markdown from '@/components/Markdown';
import { Box } from '@chakra-ui/react';
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { useSize } from 'ahooks';
import React, { useEffect, useMemo, useRef } from 'react';
const previewTypography = {
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '20px',
letterSpacing: '0.25px'
};
const isEmptyPreviewValue = (value: unknown): boolean => {
if (value === null || value === undefined) return true;
if (typeof value === 'string') return value.trim() === '';
if (Array.isArray(value)) return value.every(isEmptyPreviewValue);
if (typeof value === 'object') return Object.values(value).every(isEmptyPreviewValue);
return false;
};
const formatPreviewValue = (value?: string | null) => {
if (isEmptyPreviewValue(value)) return '';
const rawValue = value ?? '';
try {
const parsedValue = JSON.parse(rawValue);
if (isEmptyPreviewValue(parsedValue)) return '';
return JSON.stringify(parsedValue, null, 2);
} catch {
return rawValue;
}
};
const ProcessingPreviewBody = React.memo(function ProcessingPreviewBody({
content,
showAnimation
}: {
content: string;
showAnimation: boolean;
}) {
const previewRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const contentSize = useSize(contentRef);
const contentOverflow = (contentSize?.height || 0) > 80;
useEffect(() => {
if (!showAnimation || !contentOverflow) return;
previewRef.current?.scrollTo({
top: previewRef.current.scrollHeight,
behavior: 'smooth'
});
}, [content, contentOverflow, showAnimation]);
if (!content) return null;
return (
<Box position={'relative'} mt={2}>
<Box
ref={previewRef}
maxH={'80px'}
overflowY={'auto'}
color={'myGray.500'}
sx={{
'.markdown': {
...previewTypography,
wordBreak: 'normal',
overflowWrap: 'anywhere'
},
'.markdown *': {
letterSpacing: previewTypography.letterSpacing,
wordBreak: 'normal',
overflowWrap: 'anywhere'
},
'&::-webkit-scrollbar': {
display: 'none'
}
}}
css={{
scrollbarWidth: 'none'
}}
>
<Box ref={contentRef}>
<Markdown source={content} showAnimation={showAnimation} />
</Box>
</Box>
{contentOverflow && (
<Box
position={'absolute'}
left={0}
right={0}
bottom={0}
h={'32px'}
bgGradient={'linear(to-b, rgba(255,255,255,0), rgba(255,255,255,1.0))'}
pointerEvents={'none'}
/>
)}
</Box>
);
});
export const getProcessingPreviewLabelKey = (value: AIChatItemValueItemType) => {
const tool = value.tools?.[value.tools.length - 1] || value.tool;
if (tool) return tool.toolName;
if (
(value.reasoning?.content || value.agentPlanUpdate?.reasoningText) &&
!value.hideReason
) {
return 'chat:history_generating';
}
return '';
};
const RenderProcessingPreview = React.memo(function RenderProcessingPreview({
value,
showAnimation
}: {
value: AIChatItemValueItemType;
showAnimation: boolean;
}) {
const tool = value.tools?.[value.tools.length - 1] || value.tool;
const reasoningContent = value.reasoning?.content || value.agentPlanUpdate?.reasoningText || '';
const previewContent = useMemo(() => {
if (tool) return formatPreviewValue(tool.params);
return reasoningContent;
}, [tool, reasoningContent]);
if (tool && !previewContent) return null;
if (!tool && (!reasoningContent || value.hideReason)) return null;
return <ProcessingPreviewBody content={previewContent} showAnimation={showAnimation} />;
});
export default RenderProcessingPreview;
...@@ -10,9 +10,8 @@ import { ...@@ -10,9 +10,8 @@ import {
HStack HStack
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSize } from 'ahooks';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { useRef, useState } from 'react'; import React from 'react';
const reasoningTypography = { const reasoningTypography = {
fontSize: '14px', fontSize: '14px',
...@@ -26,22 +25,20 @@ const RenderReasoningContent = React.memo(function RenderReasoningContent({ ...@@ -26,22 +25,20 @@ const RenderReasoningContent = React.memo(function RenderReasoningContent({
content, content,
isChatting, isChatting,
isLastResponseValue, isLastResponseValue,
isDisabled isDisabled,
defaultExpanded = isLastResponseValue
}: { }: {
content: string; content: string;
isChatting: boolean; isChatting: boolean;
isLastResponseValue: boolean; isLastResponseValue: boolean;
isDisabled?: boolean; isDisabled?: boolean;
defaultExpanded?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const showAnimation = isChatting && isLastResponseValue; const showAnimation = isChatting && isLastResponseValue;
const [contentExpanded, setContentExpanded] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const contentSize = useSize(contentRef);
const contentOverflow = (contentSize?.height || 0) > 80;
return ( return (
<Accordion allowToggle defaultIndex={isLastResponseValue ? 0 : undefined}> <Accordion allowToggle defaultIndex={defaultExpanded ? 0 : undefined}>
<AccordionItem borderTop={'none'} borderBottom={'none'}> <AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton <AccordionButton
w={'auto'} w={'auto'}
...@@ -64,58 +61,27 @@ const RenderReasoningContent = React.memo(function RenderReasoningContent({ ...@@ -64,58 +61,27 @@ const RenderReasoningContent = React.memo(function RenderReasoningContent({
<AccordionIcon ml={1} color={'myGray.500'} /> <AccordionIcon ml={1} color={'myGray.500'} />
</AccordionButton> </AccordionButton>
<AccordionPanel py={0} pr={0} pl={0} mt={2} color={'myGray.500'}> <AccordionPanel py={0} pr={0} pl={0} mt={2} color={'myGray.500'}>
<Box <Box position={'relative'} ml={1.5}>
position={'relative'}
maxH={contentExpanded ? 'none' : '80px'}
overflow={contentExpanded ? 'visible' : 'hidden'}
ml={1.5}
pl={3}
borderLeft={'1px solid'}
borderColor={'myGray.200'}
sx={{
'.markdown': {
...reasoningTypography,
wordBreak: 'normal',
overflowWrap: 'anywhere'
},
'.markdown *': {
letterSpacing: reasoningTypography.letterSpacing,
wordBreak: 'normal',
overflowWrap: 'anywhere'
}
}}
>
<Box ref={contentRef}>
<Markdown source={content} showAnimation={showAnimation} isDisabled={isDisabled} />
</Box>
{!contentExpanded && contentOverflow && (
<Box
position={'absolute'}
left={0}
right={0}
bottom={0}
h={'32px'}
bgGradient={'linear(to-b, rgba(255,255,255,0), rgba(255,255,255,1.0))'}
pointerEvents={'none'}
/>
)}
</Box>
{!contentExpanded && contentOverflow && (
<Box <Box
as={'button'} pl={3}
type="button" borderLeft={'1px solid'}
mt={2} borderColor={'myGray.200'}
ml={1.5} sx={{
px={'8px'} '.markdown': {
color={'primary.600'} ...reasoningTypography,
fontSize={'14px'} wordBreak: 'normal',
fontWeight={500} overflowWrap: 'anywhere'
cursor={'pointer'} },
onClick={() => setContentExpanded(true)} '.markdown *': {
letterSpacing: reasoningTypography.letterSpacing,
wordBreak: 'normal',
overflowWrap: 'anywhere'
}
}}
> >
{t('chat:log.error.expand')} <Markdown source={content} showAnimation={showAnimation} isDisabled={isDisabled} />
</Box> </Box>
)} </Box>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
......
...@@ -32,8 +32,11 @@ const AIResponseBox = ({ ...@@ -32,8 +32,11 @@ const AIResponseBox = ({
onOpenCiteModal, onOpenCiteModal,
wrapProcessing = true, wrapProcessing = true,
showProcessing = true, showProcessing = true,
showFoldableProcessing: showFoldableProcessingProp = true,
showStandaloneProcessing: showStandaloneProcessingProp = true,
showAnswer = true, showAnswer = true,
showInteractive = true showInteractive = true,
defaultExpandProcessing = true
}: { }: {
chatItemDataId: string; chatItemDataId: string;
value: AIChatItemValueItemType; value: AIChatItemValueItemType;
...@@ -44,36 +47,44 @@ const AIResponseBox = ({ ...@@ -44,36 +47,44 @@ const AIResponseBox = ({
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void; onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
wrapProcessing?: boolean; wrapProcessing?: boolean;
showProcessing?: boolean; showProcessing?: boolean;
showFoldableProcessing?: boolean;
showStandaloneProcessing?: boolean;
showAnswer?: boolean; showAnswer?: boolean;
showInteractive?: boolean; showInteractive?: boolean;
defaultExpandProcessing?: boolean;
}) => { }) => {
const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus); const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus);
const showSkillReferences = useContextSelector(ChatItemContext, (v) => v.showSkillReferences); const showSkillReferences = useContextSelector(ChatItemContext, (v) => v.showSkillReferences);
const tools = value.tools || (value.tool ? [value.tool] : undefined); const tools = value.tools || (value.tool ? [value.tool] : undefined);
const disableStreamingInteraction = isChatting && isLastChild; const disableStreamingInteraction = isChatting && isLastChild;
const skills = value.skills; const skills = value.skills;
const reasoningContent = value.reasoning?.content || ''; const reasoningContent = value.reasoning?.content || value.agentPlanUpdate?.reasoningText || '';
const textContent = value.text?.content || ''; const textContent = value.text?.content || '';
if (value.hideInUI) return null; if (value.hideInUI) return null;
const responseBlocks: React.ReactNode[] = []; const responseBlocks: React.ReactNode[] = [];
const processingBlocks: React.ReactNode[] = []; const foldableProcessingBlocks: React.ReactNode[] = [];
const standaloneProcessingBlocks: React.ReactNode[] = [];
if (showProcessing && reasoningContent && !value.hideReason) { const showFoldableProcessing = showProcessing && showFoldableProcessingProp;
processingBlocks.push( const showStandaloneProcessing = showProcessing && showStandaloneProcessingProp;
if (showFoldableProcessing && reasoningContent && !value.hideReason) {
foldableProcessingBlocks.push(
<RenderReasoningContent <RenderReasoningContent
key="reasoning" key="reasoning"
isChatting={isChatting} isChatting={isChatting}
isLastResponseValue={isLastResponseValue && !textContent && !tools} isLastResponseValue={isLastResponseValue && !textContent && !tools}
content={reasoningContent} content={reasoningContent}
isDisabled={disableStreamingInteraction} isDisabled={disableStreamingInteraction}
defaultExpanded={defaultExpandProcessing && isLastResponseValue && !textContent && !tools}
/> />
); );
} }
if (showProcessing && tools && showRunningStatus) { if (showFoldableProcessing && tools && showRunningStatus) {
processingBlocks.push( foldableProcessingBlocks.push(
<Box key="tools"> <Box key="tools">
{tools.map((tool) => ( {tools.map((tool) => (
<Box key={tool.id} _notLast={{ mb: 2 }}> <Box key={tool.id} _notLast={{ mb: 2 }}>
...@@ -84,8 +95,8 @@ const AIResponseBox = ({ ...@@ -84,8 +95,8 @@ const AIResponseBox = ({
); );
} }
if (showProcessing && skills && showSkillReferences && showRunningStatus) { if (showStandaloneProcessing && skills && showSkillReferences && showRunningStatus) {
processingBlocks.push( standaloneProcessingBlocks.push(
<Box key="skills"> <Box key="skills">
{skills.map((skill) => ( {skills.map((skill) => (
<Box key={skill.id} _notLast={{ mb: 2 }}> <Box key={skill.id} _notLast={{ mb: 2 }}>
...@@ -96,29 +107,37 @@ const AIResponseBox = ({ ...@@ -96,29 +107,37 @@ const AIResponseBox = ({
); );
} }
if (showProcessing && 'plan' in value && value.plan) { if (showStandaloneProcessing && 'plan' in value && value.plan) {
processingBlocks.push(<RenderPlan key="plan" plan={value.plan} />); standaloneProcessingBlocks.push(<RenderPlan key="plan" plan={value.plan} />);
} }
if (showProcessing && 'planStatus' in value && value.planStatus?.status === 'generating') { if (
processingBlocks.push(<RenderPlanStatus key="planStatus" planStatus={value.planStatus} />); showStandaloneProcessing &&
'planStatus' in value &&
value.planStatus?.status === 'generating'
) {
standaloneProcessingBlocks.push(
<RenderPlanStatus key="planStatus" planStatus={value.planStatus} />
);
} }
if (processingBlocks.length > 0) { if (foldableProcessingBlocks.length > 0) {
if (wrapProcessing) { if (wrapProcessing) {
responseBlocks.push( responseBlocks.push(
<RenderProcessingCollapse <RenderProcessingCollapse
key="processing" key="processing"
isProcessing={isChatting && isLastResponseValue && !textContent && !value.interactive} isProcessing={isChatting && isLastResponseValue && !textContent && !value.interactive}
> >
{processingBlocks} {foldableProcessingBlocks}
</RenderProcessingCollapse> </RenderProcessingCollapse>
); );
} else { } else {
responseBlocks.push(...processingBlocks); responseBlocks.push(...foldableProcessingBlocks);
} }
} }
responseBlocks.push(...standaloneProcessingBlocks);
if (showAnswer && value.text && textContent) { if (showAnswer && value.text && textContent) {
responseBlocks.push( responseBlocks.push(
<RenderText <RenderText
...@@ -171,7 +190,7 @@ const AIResponseBox = ({ ...@@ -171,7 +190,7 @@ const AIResponseBox = ({
if (responseBlocks.length > 1) { if (responseBlocks.length > 1) {
return ( return (
<Flex flexDirection={'column'} gap={4}> <Flex flexDirection={'column'} gap={2}>
{responseBlocks} {responseBlocks}
</Flex> </Flex>
); );
......
...@@ -184,7 +184,8 @@ export const WholeResponseSideTab = ({ ...@@ -184,7 +184,8 @@ export const WholeResponseSideTab = ({
flexDirection={'column'} flexDirection={'column'}
gap={1} gap={1}
bg={isMobile ? 'myGray.100' : ''} bg={isMobile ? 'myGray.100' : ''}
m={isMobile ? 3 : 0} m={0}
mb={isMobile ? 3 : 0}
borderRadius={'md'} borderRadius={'md'}
w={isMobile ? 'auto' : '180px'} w={isMobile ? 'auto' : '180px'}
> >
......
...@@ -30,6 +30,15 @@ const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId: ...@@ -30,6 +30,15 @@ const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId:
size={'xl'} size={'xl'}
h={['90vh', '80vh']} h={['90vh', '80vh']}
maxH={['90vh', '700px']} maxH={['90vh', '700px']}
headerStyles={{
px: [5, 8],
pt: [6, 8]
}}
bodyStyles={{
px: [5, 8],
pt: [4, 6],
pb: [5, 8]
}}
title={ title={
<Flex alignItems={'center'} gap={2}> <Flex alignItems={'center'} gap={2}>
<Box fontSize={'20px'} lineHeight={'26px'} letterSpacing={'0.15px'} fontWeight={500}> <Box fontSize={'20px'} lineHeight={'26px'} letterSpacing={'0.15px'} fontWeight={500}>
......
...@@ -136,7 +136,8 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props ...@@ -136,7 +136,8 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
position={'relative'} position={'relative'}
flexDirection={'column'} flexDirection={'column'}
h={'full'} h={'full'}
py={4} pt={4}
pb={0}
{...cardStyles} {...cardStyles}
boxShadow={'3'} boxShadow={'3'}
> >
......
...@@ -74,7 +74,8 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => { ...@@ -74,7 +74,8 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
position={'relative'} position={'relative'}
flexDirection={'column'} flexDirection={'column'}
h={'full'} h={'full'}
py={4} pt={4}
pb={0}
{...cardStyles} {...cardStyles}
boxShadow={'3'} boxShadow={'3'}
> >
......
...@@ -46,6 +46,10 @@ const ChatFavouriteApp = ({ hideMobileHeader = false, mobileSearchKey }: Props) ...@@ -46,6 +46,10 @@ const ChatFavouriteApp = ({ hideMobileHeader = false, mobileSearchKey }: Props)
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const upsertRecentlyUsedAppPlaceholder = useContextSelector(
ChatPageContext,
(v) => v.upsertRecentlyUsedAppPlaceholder
);
const wideLogoUrl = useContextSelector(ChatPageContext, (v) => v.chatSettings?.wideLogoUrl); const wideLogoUrl = useContextSelector(ChatPageContext, (v) => v.chatSettings?.wideLogoUrl);
const homeTabTitle = useContextSelector(ChatPageContext, (v) => v.chatSettings?.homeTabTitle); const homeTabTitle = useContextSelector(ChatPageContext, (v) => v.chatSettings?.homeTabTitle);
...@@ -255,7 +259,14 @@ const ChatFavouriteApp = ({ hideMobileHeader = false, mobileSearchKey }: Props) ...@@ -255,7 +259,14 @@ const ChatFavouriteApp = ({ hideMobileHeader = false, mobileSearchKey }: Props)
_hover={{ _hover={{
borderColor: 'primary.300' borderColor: 'primary.300'
}} }}
onClick={() => handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app.appId)} onClick={() => {
upsertRecentlyUsedAppPlaceholder({
appId: app.appId,
name: app.name,
avatar: app.avatar
});
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app.appId);
}}
> >
<HStack minW={0}> <HStack minW={0}>
<Avatar src={app.avatar} borderRadius={'sm'} w={'1.5rem'} /> <Avatar src={app.avatar} borderRadius={'sm'} w={'1.5rem'} />
......
...@@ -119,6 +119,10 @@ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; app ...@@ -119,6 +119,10 @@ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; app
const { t } = useTranslation(); const { t } = useTranslation();
const myApps = useContextSelector(ChatPageContext, (v) => v.myApps); const myApps = useContextSelector(ChatPageContext, (v) => v.myApps);
const upsertRecentlyUsedAppPlaceholder = useContextSelector(
ChatPageContext,
(v) => v.upsertRecentlyUsedAppPlaceholder
);
const [currentTab, setCurrentTab] = useState<TabEnum>(TabEnum.recently); const [currentTab, setCurrentTab] = useState<TabEnum>(TabEnum.recently);
...@@ -136,6 +140,10 @@ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; app ...@@ -136,6 +140,10 @@ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; app
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const onclickApp = (id: string) => { const onclickApp = (id: string) => {
const app = myApps.find((item) => item.appId === id);
if (app) {
upsertRecentlyUsedAppPlaceholder(app);
}
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id); handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id);
onCloseDrawer(); onCloseDrawer();
}; };
...@@ -225,6 +233,11 @@ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; app ...@@ -225,6 +233,11 @@ const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; app
value={appId} value={appId}
onSelect={(item) => { onSelect={(item) => {
if (!item) return; if (!item) return;
upsertRecentlyUsedAppPlaceholder({
appId: item.id,
name: item.name,
avatar: item.avatar
});
onclickApp(item.id); onclickApp(item.id);
}} }}
server={getAppList} server={getAppList}
......
...@@ -17,9 +17,11 @@ const ChatQuoteList = ({ ...@@ -17,9 +17,11 @@ const ChatQuoteList = ({
onClose: () => void; onClose: () => void;
}) => { }) => {
const [activeMetadata, setActiveMetadata] = useState<GetQuoteProps>(metadata); const [activeMetadata, setActiveMetadata] = useState<GetQuoteProps>(metadata);
const [canBackToQuoteList, setCanBackToQuoteList] = useState(false);
useEffect(() => { useEffect(() => {
setActiveMetadata(metadata); setActiveMetadata(metadata);
setCanBackToQuoteList(false);
}, [metadata]); }, [metadata]);
if ('collectionId' in activeMetadata) { if ('collectionId' in activeMetadata) {
...@@ -28,7 +30,14 @@ const ChatQuoteList = ({ ...@@ -28,7 +30,14 @@ const ChatQuoteList = ({
rawSearch={rawSearch} rawSearch={rawSearch}
metadata={activeMetadata} metadata={activeMetadata}
onClose={onClose} onClose={onClose}
onBack={'collectionIdList' in metadata ? () => setActiveMetadata(metadata) : undefined} onBack={
canBackToQuoteList
? () => {
setActiveMetadata(metadata);
setCanBackToQuoteList(false);
}
: undefined
}
/> />
); );
} else if ('collectionIdList' in activeMetadata) { } else if ('collectionIdList' in activeMetadata) {
...@@ -39,6 +48,7 @@ const ChatQuoteList = ({ ...@@ -39,6 +48,7 @@ const ChatQuoteList = ({
onClose={onClose} onClose={onClose}
onOpenCollectionQuote={(nextMetadata: GetCollectionQuoteDataProps) => { onOpenCollectionQuote={(nextMetadata: GetCollectionQuoteDataProps) => {
setActiveMetadata(nextMetadata); setActiveMetadata(nextMetadata);
setCanBackToQuoteList(true);
}} }}
/> />
); );
......
...@@ -34,6 +34,10 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { ...@@ -34,6 +34,10 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
) )
); );
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const upsertRecentlyUsedAppPlaceholder = useContextSelector(
ChatPageContext,
(v) => v.upsertRecentlyUsedAppPlaceholder
);
return ( return (
<> <>
...@@ -89,6 +93,11 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { ...@@ -89,6 +93,11 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
} }
}); });
} else { } else {
upsertRecentlyUsedAppPlaceholder({
appId: app._id,
name: app.name,
avatar: app.avatar
});
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app._id); handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app._id);
} }
}} }}
......
...@@ -30,7 +30,6 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; ...@@ -30,7 +30,6 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import ChatWindowHeader from './ChatWindowHeader'; import ChatWindowHeader from './ChatWindowHeader';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import ToolMenu from '@/pageComponents/chat/ToolMenu'; import ToolMenu from '@/pageComponents/chat/ToolMenu';
import { mobileChatHeaderIconButtonStyle } from './headerIconButtonStyle'; import { mobileChatHeaderIconButtonStyle } from './headerIconButtonStyle';
...@@ -52,6 +51,8 @@ const AppChatWindow = () => { ...@@ -52,6 +51,8 @@ const AppChatWindow = () => {
const showSkillReferences = useContextSelector(ChatItemContext, (v) => v.showSkillReferences); const showSkillReferences = useContextSelector(ChatItemContext, (v) => v.showSkillReferences);
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const chatWindowTitle =
chatBoxData.title?.trim() || t('common:core.chat.New Chat', { defaultValue: '新对话' });
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
...@@ -61,8 +62,13 @@ const AppChatWindow = () => { ...@@ -61,8 +62,13 @@ const AppChatWindow = () => {
const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId; const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId;
const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed); const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed);
const upsertRecentlyUsedAppPlaceholder = useContextSelector(
ChatPageContext,
(v) => v.upsertRecentlyUsedAppPlaceholder
);
const { loading } = useRequest( const { loading } = useRequest(
async () => { async () => {
...@@ -72,6 +78,14 @@ const AppChatWindow = () => { ...@@ -72,6 +78,14 @@ const AppChatWindow = () => {
res.userAvatar = userInfo?.avatar; res.userAvatar = userInfo?.avatar;
setChatBoxData(res); setChatBoxData(res);
const isHomeApp = pane === ChatSidebarPaneEnum.HOME || res.appId === chatSettings?.appId;
if (!isHomeApp) {
upsertRecentlyUsedAppPlaceholder({
appId: res.appId,
name: res.app.name,
avatar: res.app.avatar
});
}
resetVariables({ resetVariables({
variables: res.variables, variables: res.variables,
...@@ -142,12 +156,15 @@ const AppChatWindow = () => { ...@@ -142,12 +156,15 @@ const AppChatWindow = () => {
[ [
appId, appId,
chatId, chatId,
pane,
chatSettings?.appId,
onUpdateHistoryTitle, onUpdateHistoryTitle,
setChatBoxData, setChatBoxData,
forbidLoadChatRef, forbidLoadChatRef,
isShowCite, isShowCite,
showSkillReferences, showSkillReferences,
refreshRecentlyUsed refreshRecentlyUsed,
upsertRecentlyUsedAppPlaceholder
] ]
); );
...@@ -183,7 +200,7 @@ const AppChatWindow = () => { ...@@ -183,7 +200,7 @@ const AppChatWindow = () => {
> >
{isPc ? ( {isPc ? (
<ChatWindowHeader <ChatWindowHeader
title={chatBoxData.title} title={chatWindowTitle}
history={chatRecords} history={chatRecords}
chatType={ChatTypeEnum.chat} chatType={ChatTypeEnum.chat}
/> />
...@@ -205,15 +222,13 @@ const AppChatWindow = () => { ...@@ -205,15 +222,13 @@ const AppChatWindow = () => {
/> />
<Flex alignItems="center" minW={0} flex="1" justifyContent="center" px={3}> <Flex alignItems="center" minW={0} flex="1" justifyContent="center" px={3}>
<Avatar src={chatBoxData.app.avatar} w="20px" borderRadius="6px" />
<Box <Box
ml={2}
fontSize="16px" fontSize="16px"
fontWeight={500} fontWeight={500}
color="myGray.900" color="myGray.900"
className="textEllipsis" className="textEllipsis"
> >
{chatBoxData.app.name} {chatWindowTitle}
</Box> </Box>
</Flex> </Flex>
...@@ -223,7 +238,7 @@ const AppChatWindow = () => { ...@@ -223,7 +238,7 @@ const AppChatWindow = () => {
</Flex> </Flex>
)} )}
<Box flex={'1 0 0'} bg={'white'} pb={4}> <Box flex={'1 0 0'} bg={'white'}>
{isPlugin ? ( {isPlugin ? (
<CustomPluginRunBox <CustomPluginRunBox
appId={appId} appId={appId}
......
...@@ -71,6 +71,37 @@ const ModelOptionLabel = React.memo(function ModelOptionLabel({ ...@@ -71,6 +71,37 @@ const ModelOptionLabel = React.memo(function ModelOptionLabel({
); );
}); });
const SelectedModelLabel = React.memo(function SelectedModelLabel({
model,
avatar,
avatarSize,
noOfLines
}: {
model: SystemModelItemType;
avatar?: string;
avatarSize: string;
noOfLines?: ResponsiveValue<number>;
}) {
return (
<Flex alignItems={'center'} py={1} minW={0} overflow={'hidden'} w={'100%'}>
<Avatar
borderRadius={'0'}
mr={2}
src={avatar || HUGGING_FACE_ICON}
w={avatarSize}
fallbackSrc={HUGGING_FACE_ICON}
flexShrink={0}
/>
<ModelOptionLabel
name={model.name}
noOfLines={noOfLines}
showTestModeTip={isTestModeModel(model)}
showMultimodalTip={isMultimodalEmbeddingModel(model)}
/>
</Flex>
);
});
const OneRowSelector = ({ const OneRowSelector = ({
list, list,
onChange, onChange,
...@@ -173,14 +204,12 @@ const OneRowSelector = ({ ...@@ -173,14 +204,12 @@ const OneRowSelector = ({
list={avatarList} list={avatarList}
valueLabel={ valueLabel={
selectedModelData ? ( selectedModelData ? (
<Flex alignItems={'center'} py={1} minW={0} overflow={'hidden'} w={'100%'}> <SelectedModelLabel
<ModelOptionLabel model={selectedModelData}
name={selectedModelData.name} avatar={getModelProvider(selectedModelData.provider)?.avatar}
noOfLines={noOfLines} avatarSize={avatarSize}
showTestModeTip={isTestModeModel(selectedModelData)} noOfLines={noOfLines}
showMultimodalTip={isMultimodalEmbeddingModel(selectedModelData)} />
/>
</Flex>
) : undefined ) : undefined
} }
placeholder={loading ? t('common:model_loading') : t('common:not_model_config')} placeholder={loading ? t('common:model_loading') : t('common:not_model_config')}
...@@ -212,6 +241,7 @@ const MultipleRowSelector = ({ ...@@ -212,6 +241,7 @@ const MultipleRowSelector = ({
ttsModelList, ttsModelList,
sttModelList, sttModelList,
reRankModelList, reRankModelList,
getModelProvider,
getModelProviders, getModelProviders,
getMyModelList getMyModelList
} = useSystemStore(); } = useSystemStore();
...@@ -311,16 +341,14 @@ const MultipleRowSelector = ({ ...@@ -311,16 +341,14 @@ const MultipleRowSelector = ({
if (!selectedModelData) return <>{t('common:not_model_config')}</>; if (!selectedModelData) return <>{t('common:not_model_config')}</>;
return ( return (
<Flex alignItems={'center'} py={1} minW={0} overflow={'hidden'} w={'100%'}> <SelectedModelLabel
<ModelOptionLabel model={selectedModelData}
name={selectedModelData.name} avatar={getModelProvider(selectedModelData.provider)?.avatar}
noOfLines={noOfLines} avatarSize={avatarSize}
showTestModeTip={isTestModeModel(selectedModelData)} noOfLines={noOfLines}
showMultimodalTip={isMultimodalEmbeddingModel(selectedModelData)} />
/>
</Flex>
); );
}, [loading, props.value, t, selectedModelData, noOfLines]); }, [loading, props.value, t, selectedModelData, getModelProvider, avatarSize, noOfLines]);
return ( return (
<Box <Box
......
...@@ -291,10 +291,12 @@ const HomeChatWindow = () => { ...@@ -291,10 +291,12 @@ const HomeChatWindow = () => {
<> <>
{/* 模型选择 */} {/* 模型选择 */}
{isPc && availableModels.length > 0 && ( {isPc && availableModels.length > 0 && (
<Box w={['0', '128px']} flex={['1 1 0', '0 0 128px']} minW={0}> <Box w={'fit-content'} maxW={'300px'} flex={'0 1 auto'} minW={0}>
<ChatAIModelSelector <ChatAIModelSelector
cacheModel={false} cacheModel={false}
h={'36px'} h={'36px'}
w={'fit-content'}
maxW={'300px'}
boxShadow={'none'} boxShadow={'none'}
size="sm" size="sm"
bg={'myGray.50'} bg={'myGray.50'}
......
...@@ -77,24 +77,28 @@ const MobileModelSelectorDrawer = ({ isOpen, modelList, value, onChange, onClose ...@@ -77,24 +77,28 @@ const MobileModelSelectorDrawer = ({ isOpen, modelList, value, onChange, onClose
bottom: 0, bottom: 0,
left: 0, left: 0,
zIndex: 1401, zIndex: 1401,
outline: 'none' outline: 'none',
height: 'fit-content',
maxHeight: 'min(82dvh, 560px)'
}} }}
> >
<Box <Box
bg="white" bg="white"
borderTopRadius="16px" borderTopRadius="16px"
px={4} px={4}
pb="42px" pb="calc(16px + env(safe-area-inset-bottom))"
maxH="min(72dvh, 560px)" h="fit-content"
maxH="min(82dvh, 560px)"
display="flex" display="flex"
flexDirection="column" flexDirection="column"
overflow="hidden"
> >
<Flex justifyContent="center" py="16px" flexShrink={0}> <Flex justifyContent="center" py="16px" flexShrink={0}>
<Drawer.Handle style={{ backgroundColor: 'var(--chakra-colors-myGray-400)' }} /> <Drawer.Handle style={{ backgroundColor: 'var(--chakra-colors-myGray-400)' }} />
</Flex> </Flex>
{!activeProvider ? ( {!activeProvider ? (
<Box pb={4} flex="1 1 0" minH={0} overflowY="auto"> <Box pb={4} flex="0 1 auto" minH={0} overflowY="auto">
{providerGroups.map((provider) => ( {providerGroups.map((provider) => (
<Flex <Flex
key={provider.id} key={provider.id}
...@@ -124,7 +128,7 @@ const MobileModelSelectorDrawer = ({ isOpen, modelList, value, onChange, onClose ...@@ -124,7 +128,7 @@ const MobileModelSelectorDrawer = ({ isOpen, modelList, value, onChange, onClose
))} ))}
</Box> </Box>
) : ( ) : (
<Flex flexDirection="column" flex="1 1 0" minH={0}> <Flex flexDirection="column" flex="0 1 auto" minH={0}>
<Flex h="48px" alignItems="center" flexShrink={0}> <Flex h="48px" alignItems="center" flexShrink={0}>
<IconButton <IconButton
aria-label="Back" aria-label="Back"
...@@ -164,7 +168,7 @@ const MobileModelSelectorDrawer = ({ isOpen, modelList, value, onChange, onClose ...@@ -164,7 +168,7 @@ const MobileModelSelectorDrawer = ({ isOpen, modelList, value, onChange, onClose
onClick={onClose} onClick={onClose}
/> />
</Flex> </Flex>
<Box pb={4} flex="1 1 0" minH={0} overflowY="auto"> <Box pb={4} flex="0 1 auto" minH={0} overflowY="auto">
{activeProvider.children.map((model) => { {activeProvider.children.map((model) => {
const isSelected = model.model === value; const isSelected = model.model === value;
......
...@@ -11,7 +11,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; ...@@ -11,7 +11,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { DEFAULT_LOGO_BANNER_URL } from '@/pageComponents/chat/constants'; import { DEFAULT_LOGO_BANNER_URL } from '@/pageComponents/chat/constants';
import ChatSliderQuickAppList from '@/pageComponents/chat/slider/ChatSliderQuickAppList'; import ChatSliderRecentAppList from '@/pageComponents/chat/slider/ChatSliderQuickAppList';
type Props = { type Props = {
title?: string; title?: string;
...@@ -69,8 +69,8 @@ const ChatSliderHeader = ({ title, banner }: Props) => { ...@@ -69,8 +69,8 @@ const ChatSliderHeader = ({ title, banner }: Props) => {
bg={isHomePane ? 'primary.100' : 'transparent'} bg={isHomePane ? 'primary.100' : 'transparent'}
color={isHomePane ? 'primary.600' : 'myGray.500'} color={isHomePane ? 'primary.600' : 'myGray.500'}
_hover={{ _hover={{
bg: 'primary.100', bg: isHomePane ? 'primary.100' : 'transparent',
color: 'primary.600' color: isHomePane ? 'primary.600' : 'myGray.500'
}} }}
onClick={() => { onClick={() => {
handlePaneChange(ChatSidebarPaneEnum.HOME); handlePaneChange(ChatSidebarPaneEnum.HOME);
...@@ -94,8 +94,8 @@ const ChatSliderHeader = ({ title, banner }: Props) => { ...@@ -94,8 +94,8 @@ const ChatSliderHeader = ({ title, banner }: Props) => {
bg={isAllAppsPane ? 'primary.100' : 'transparent'} bg={isAllAppsPane ? 'primary.100' : 'transparent'}
color={isAllAppsPane ? 'primary.600' : 'myGray.500'} color={isAllAppsPane ? 'primary.600' : 'myGray.500'}
_hover={{ _hover={{
bg: 'primary.100', bg: isAllAppsPane ? 'primary.100' : 'transparent',
color: 'primary.600' color: isAllAppsPane ? 'primary.600' : 'myGray.500'
}} }}
onClick={() => { onClick={() => {
handlePaneChange(ChatSidebarPaneEnum.ALL_APPS); handlePaneChange(ChatSidebarPaneEnum.ALL_APPS);
...@@ -109,7 +109,7 @@ const ChatSliderHeader = ({ title, banner }: Props) => { ...@@ -109,7 +109,7 @@ const ChatSliderHeader = ({ title, banner }: Props) => {
</Box> </Box>
</Flex> </Flex>
<ChatSliderQuickAppList /> <ChatSliderRecentAppList />
</Flex> </Flex>
</> </>
); );
......
...@@ -7,30 +7,32 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore'; ...@@ -7,30 +7,32 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
/** /**
* 移动端侧边抽屉内的快捷应用入口。 * 移动端侧边抽屉内的最近使用应用入口。
* 点击后进入普通应用聊天,保持“首页”菜单只表示首页聊天本身。 * 仅展示最近 2 个真实使用的应用;当前正在使用的应用会由页面上下文提前占位到列表前方。
*/ */
const ChatSliderQuickAppList = () => { const ChatSliderRecentAppList = () => {
const { appId: activeAppId, setChatId } = useChatStore(); const { appId: activeAppId, setChatId } = useChatStore();
const quickAppList = useContextSelector( const recentlyUsedApps = useContextSelector(ChatPageContext, (v) => v.myApps.slice(0, 2));
ChatPageContext,
(v) => v.chatSettings?.quickAppList || []
);
const pane = useContextSelector(ChatPageContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
const upsertRecentlyUsedAppPlaceholder = useContextSelector(
ChatPageContext,
(v) => v.upsertRecentlyUsedAppPlaceholder
);
const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider);
if (quickAppList.length === 0) return null; if (recentlyUsedApps.length === 0) return null;
return ( return (
<Flex flexDir="column" gap="4px"> <Flex flexDir="column" gap="4px">
{quickAppList.map((app) => { {recentlyUsedApps.map((app) => {
const isActive = pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && activeAppId === app._id; const isActive =
pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && activeAppId === app.appId;
return ( return (
<Flex <Flex
key={app._id} key={app.appId}
p="8px" p="8px"
gap={2} gap={2}
h="44px" h="44px"
...@@ -45,7 +47,8 @@ const ChatSliderQuickAppList = () => { ...@@ -45,7 +47,8 @@ const ChatSliderQuickAppList = () => {
color: 'primary.600' color: 'primary.600'
}} }}
onClick={() => { onClick={() => {
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app._id); upsertRecentlyUsedAppPlaceholder(app);
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app.appId);
onCloseSlider(); onCloseSlider();
setChatId(); setChatId();
}} }}
...@@ -61,4 +64,4 @@ const ChatSliderQuickAppList = () => { ...@@ -61,4 +64,4 @@ const ChatSliderQuickAppList = () => {
); );
}; };
export default ChatSliderQuickAppList; export default ChatSliderRecentAppList;
...@@ -233,7 +233,8 @@ const ActionButton: React.FC<{ ...@@ -233,7 +233,8 @@ const ActionButton: React.FC<{
bg: 'transparent', bg: 'transparent',
color: 'myGray.500', color: 'myGray.500',
_hover: { _hover: {
bg: isCollapsed ? 'myGray.200' : 'primary.100' bg: 'transparent',
color: 'myGray.500'
} }
})} })}
onClick={onClick} onClick={onClick}
...@@ -281,7 +282,7 @@ const NavigationSection = () => { ...@@ -281,7 +282,7 @@ const NavigationSection = () => {
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{isCollapsed ? ( {isCollapsed ? (
<AnimatedSection show={true}> <AnimatedSection show={true}>
<Flex flexDir="column" gap={'12px'}> <Flex flexDir="column" gap={0}>
{feConfigs.isPlus && ( {feConfigs.isPlus && (
<> <>
{isEnableHome && ( {isEnableHome && (
...@@ -305,7 +306,7 @@ const NavigationSection = () => { ...@@ -305,7 +306,7 @@ const NavigationSection = () => {
</AnimatedSection> </AnimatedSection>
) : ( ) : (
<AnimatedSection show={true}> <AnimatedSection show={true}>
<Flex flexDir="column" gap={'12px'}> <Flex flexDir="column" gap={0}>
{feConfigs.isPlus && ( {feConfigs.isPlus && (
<> <>
{isEnableHome && ( {isEnableHome && (
...@@ -473,6 +474,10 @@ const ChatSlider = ({ activeAppId }: Props) => { ...@@ -473,6 +474,10 @@ const ChatSlider = ({ activeAppId }: Props) => {
const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1);
const pane = useContextSelector(ChatPageContext, (v) => v.pane); const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const myApps = useContextSelector(ChatPageContext, (v) => v.myApps); const myApps = useContextSelector(ChatPageContext, (v) => v.myApps);
const upsertRecentlyUsedAppPlaceholder = useContextSelector(
ChatPageContext,
(v) => v.upsertRecentlyUsedAppPlaceholder
);
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
...@@ -530,8 +535,10 @@ const ChatSlider = ({ activeAppId }: Props) => { ...@@ -530,8 +535,10 @@ const ChatSlider = ({ activeAppId }: Props) => {
? { bg: 'primary.100', color: 'primary.600' } ? { bg: 'primary.100', color: 'primary.600' }
: { : {
_hover: { bg: 'primary.100' }, _hover: { bg: 'primary.100' },
onClick: () => onClick: () => {
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item.appId) upsertRecentlyUsedAppPlaceholder(item);
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item.appId);
}
})} })}
> >
<Avatar src={item.avatar} w={'20px'} h={'20px'} borderRadius={'6px'} /> <Avatar src={item.avatar} w={'20px'} h={'20px'} borderRadius={'6px'} />
......
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex, IconButton } from '@chakra-ui/react';
import { streamFetch } from '@/web/common/api/fetch'; import { streamFetch } from '@/web/common/api/fetch';
import SideBar from '@/components/SideBar'; import SideBar from '@/components/SideBar';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
...@@ -9,7 +9,6 @@ import ChatBox from '@/components/core/chat/ChatContainer/ChatBox'; ...@@ -9,7 +9,6 @@ import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type'; import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import PageContainer from '@/components/PageContainer'; import PageContainer from '@/components/PageContainer';
import ChatHeader from '@/pageComponents/chat/ChatHeader';
import { serviceSideProps } from '@/web/common/i18n/utils'; import { serviceSideProps } from '@/web/common/i18n/utils';
import { LANG_KEY, SHARE_LANG_KEY } from '@fastgpt/web/i18n/utils'; import { LANG_KEY, SHARE_LANG_KEY } from '@fastgpt/web/i18n/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -39,11 +38,14 @@ import { type AppSchemaType } from '@fastgpt/global/core/app/type'; ...@@ -39,11 +38,14 @@ import { type AppSchemaType } from '@fastgpt/global/core/app/type';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList'; import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar';
import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import ChatLanguageSelector from '@/pageComponents/chat/LanguageSelector'; import ChatLanguageSelector from '@/pageComponents/chat/LanguageSelector';
import ChatWindowHeader from '@/pageComponents/chat/ChatWindow/ChatWindowHeader';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ToolMenu from '@/pageComponents/chat/ToolMenu';
import { mobileChatHeaderIconButtonStyle } from '@/pageComponents/chat/ChatWindow/headerIconButtonStyle';
const logger = getLogger(LogCategories.MODULE.CHAT.ITEM); const logger = getLogger(LogCategories.MODULE.CHAT.ITEM);
...@@ -96,18 +98,21 @@ const OutLink = (props: Props) => { ...@@ -96,18 +98,21 @@ const OutLink = (props: Props) => {
const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat); const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle); const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle);
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin); const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData); const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite); const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const chatWindowTitle =
chatBoxData.title?.trim() || t('common:core.chat.New Chat', { defaultValue: '新对话' });
const initSign = useRef(false); const initSign = useRef(false);
const { data, loading } = useRequest( const { data, loading } = useRequest(
...@@ -303,17 +308,67 @@ const OutLink = (props: Props) => { ...@@ -303,17 +308,67 @@ const OutLink = (props: Props) => {
flexDirection={'column'} flexDirection={'column'}
> >
{/* header */} {/* header */}
{showHead === '1' ? ( {showHead === '1' &&
<ChatHeader (isPc ? (
chatSettings={undefined} !isPlugin && (
pane={ChatSidebarPaneEnum.RECENTLY_USED_APPS} <ChatWindowHeader
history={chatRecords} title={chatWindowTitle}
totalRecordsCount={totalRecordsCount} history={chatRecords}
showHistory={showHistory === '1'} chatType={ChatTypeEnum.share}
reserveSpace={showWorkorder !== undefined} />
hideMenu={hideMenu === '1'} )
/> ) : (
) : null} <Flex
h="48px"
px={4}
bg="white"
alignItems="center"
justifyContent="space-between"
color="myGray.600"
>
{showHistory === '1' ? (
<IconButton
aria-label="Open history"
icon={
<MyIcon
name="core/chat/sidebar/menu"
w="20px"
h="20px"
color="currentColor"
/>
}
variant="unstyled"
{...mobileChatHeaderIconButtonStyle}
onClick={onOpenSlider}
/>
) : (
<Box minW="36px" />
)}
<Flex alignItems="center" minW={0} flex="1" justifyContent="center" px={3}>
<Box
fontSize="16px"
fontWeight={500}
color="myGray.900"
className="textEllipsis"
>
{chatWindowTitle}
</Box>
</Flex>
{hideMenu === '1' ? (
<Box minW="36px" />
) : (
<Box minW="36px">
<ToolMenu
history={chatRecords}
reserveSpace={showWorkorder !== undefined}
chatType={ChatTypeEnum.share}
/>
</Box>
)}
</Flex>
))}
{/* chat box */} {/* chat box */}
<Box flex={1} bg={'white'}> <Box flex={1} bg={'white'}>
{isPlugin ? ( {isPlugin ? (
......
...@@ -87,7 +87,30 @@ const ChatContextProvider = ({ ...@@ -87,7 +87,30 @@ const ChatContextProvider = ({
const { chatId, setChatId, outLinkAuthData } = useChatStore(); const { chatId, setChatId, outLinkAuthData } = useChatStore();
const historyAppId = String(params.appId ?? ''); const historyAppId = String(params.appId ?? '');
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const {
isOpen: isOpenSlider,
onClose: onCloseSlider,
onOpen: openSlider
} = useDisclosure();
const openSliderTimerRef = useRef<ReturnType<typeof setTimeout>>();
const onOpenSlider = useCallback(() => {
if (openSliderTimerRef.current) {
clearTimeout(openSliderTimerRef.current);
}
openSliderTimerRef.current = setTimeout(() => {
openSlider();
openSliderTimerRef.current = undefined;
}, 0);
}, [openSlider]);
useEffect(() => {
return () => {
if (openSliderTimerRef.current) {
clearTimeout(openSliderTimerRef.current);
}
};
}, []);
const { const {
ScrollData, ScrollData,
......
...@@ -19,6 +19,8 @@ import { useLatest, useMount } from 'ahooks'; ...@@ -19,6 +19,8 @@ import { useLatest, useMount } from 'ahooks';
import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/global/openapi/core/chat/api'; import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/global/openapi/core/chat/api';
import type { UserType } from '@fastgpt/global/support/user/type'; import type { UserType } from '@fastgpt/global/support/user/type';
type RecentlyUsedAppPlaceholderInput = Partial<GetRecentlyUsedAppsResponseType[number]>;
export type ChatPageContextValue = { export type ChatPageContextValue = {
// Pane & collapse // Pane & collapse
pane: ChatSidebarPaneEnum; pane: ChatSidebarPaneEnum;
...@@ -38,6 +40,7 @@ export type ChatPageContextValue = { ...@@ -38,6 +40,7 @@ export type ChatPageContextValue = {
isInitedUser: boolean; isInitedUser: boolean;
userInfo: UserType | null; userInfo: UserType | null;
myApps: GetRecentlyUsedAppsResponseType; myApps: GetRecentlyUsedAppsResponseType;
upsertRecentlyUsedAppPlaceholder: (app: RecentlyUsedAppPlaceholderInput) => void;
refreshRecentlyUsed: () => void; refreshRecentlyUsed: () => void;
}; };
...@@ -54,6 +57,7 @@ export const ChatPageContext = createContext<ChatPageContextValue>({ ...@@ -54,6 +57,7 @@ export const ChatPageContext = createContext<ChatPageContextValue>({
isInitedUser: false, isInitedUser: false,
userInfo: null, userInfo: null,
myApps: [], myApps: [],
upsertRecentlyUsedAppPlaceholder: () => {},
refreshRecentlyUsed: () => {} refreshRecentlyUsed: () => {}
}); });
...@@ -74,6 +78,8 @@ export const ChatPageContextProvider = ({ ...@@ -74,6 +78,8 @@ export const ChatPageContextProvider = ({
}; };
const [collapse, setCollapse] = useState<CollapseStatusType>(defaultCollapseStatus); const [collapse, setCollapse] = useState<CollapseStatusType>(defaultCollapseStatus);
const [recentlyUsedAppPlaceholders, setRecentlyUsedAppPlaceholders] =
useState<GetRecentlyUsedAppsResponseType>([]);
// Get recently used apps // Get recently used apps
const { data: myApps = [], refresh: refreshRecentlyUsed } = useRequest( const { data: myApps = [], refresh: refreshRecentlyUsed } = useRequest(
...@@ -129,6 +135,36 @@ export const ChatPageContextProvider = ({ ...@@ -129,6 +135,36 @@ export const ChatPageContextProvider = ({
} }
); );
const homeAppId = chatSettings?.appId;
const upsertRecentlyUsedAppPlaceholder = useCallback(
(app: RecentlyUsedAppPlaceholderInput) => {
const { appId, name, avatar } = app;
if (!appId || !name || !avatar) return;
if (appId === homeAppId) return;
setRecentlyUsedAppPlaceholders((state) => [
{
appId,
name,
avatar
},
...state.filter((item) => item.appId !== appId)
]);
},
[homeAppId]
);
const mergedMyApps = useMemo(() => {
const appMap = new Map<string, GetRecentlyUsedAppsResponseType[number]>();
[...recentlyUsedAppPlaceholders, ...myApps].forEach((app) => {
if (app.appId === homeAppId) return;
appMap.set(app.appId, app);
});
return Array.from(appMap.values());
}, [homeAppId, myApps, recentlyUsedAppPlaceholders]);
const lastestPane = useLatest(pane); const lastestPane = useLatest(pane);
const handlePaneChange = useCallback( const handlePaneChange = useCallback(
async (newPane: ChatSidebarPaneEnum, id?: string, tab?: ChatSettingTabOptionEnum) => { async (newPane: ChatSidebarPaneEnum, id?: string, tab?: ChatSettingTabOptionEnum) => {
...@@ -194,7 +230,8 @@ export const ChatPageContextProvider = ({ ...@@ -194,7 +230,8 @@ export const ChatPageContextProvider = ({
logos, logos,
isInitedUser: true, isInitedUser: true,
userInfo, userInfo,
myApps, myApps: mergedMyApps,
upsertRecentlyUsedAppPlaceholder,
refreshRecentlyUsed refreshRecentlyUsed
}), }),
[ [
...@@ -206,7 +243,8 @@ export const ChatPageContextProvider = ({ ...@@ -206,7 +243,8 @@ export const ChatPageContextProvider = ({
refreshChatSetting, refreshChatSetting,
logos, logos,
userInfo, userInfo,
myApps, mergedMyApps,
upsertRecentlyUsedAppPlaceholder,
refreshRecentlyUsed refreshRecentlyUsed
] ]
); );
......
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