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