Fix:webapp UI issues (#15601)

This commit is contained in:
KVOJJJin 2025-03-13 14:23:41 +08:00 committed by GitHub
parent 5e035a4209
commit efebbffe96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 182 additions and 50 deletions

View File

@ -19,6 +19,8 @@ import {
} from '@/service/share'
import AppIcon from '@/app/components/base/app-icon'
import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames'
const ChatWrapper = () => {
@ -39,6 +41,10 @@ const ChatWrapper = () => {
currentChatInstanceRef,
appData,
themeBuilder,
sidebarCollapseState,
clearChatList,
setClearChatList,
setIsResponding,
} = useChatWithHistoryContext()
const appConfig = useMemo(() => {
const config = appParams || {}
@ -58,7 +64,7 @@ const ChatWrapper = () => {
setTargetMessageId,
handleSend,
handleStop,
isResponding,
isResponding: respondingState,
suggestedQuestions,
} = useChat(
appConfig,
@ -68,6 +74,8 @@ const ChatWrapper = () => {
},
appPrevChatTree,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
clearChatList,
setClearChatList,
)
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
const inputDisabled = useMemo(() => {
@ -108,6 +116,10 @@ const ChatWrapper = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setIsResponding(respondingState)
}, [respondingState, setIsResponding])
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
const data: any = {
query: message,
@ -166,12 +178,33 @@ const ChatWrapper = () => {
const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
if (respondingState)
return null
if (currentConversationId)
return null
if (!welcomeMessage)
return null
if (!collapsed && inputsForms.length > 0)
return null
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return (
<div className='h-[50vh] py-12 px-4 flex items-center justify-center'>
<div className='grow max-w-[720px] flex gap-4'>
<AppIcon
size='xl'
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='grow px-4 py-3 bg-chat-bubble-bg text-text-primary rounded-2xl body-lg-regular'>
<Markdown content={welcomeMessage.content} />
<SuggestedQuestions item={welcomeMessage} />
</div>
</div>
</div>
)
}
return (
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
<AppIcon
@ -181,10 +214,10 @@ const ChatWrapper = () => {
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
<Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} />
</div>
)
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon
@ -203,10 +236,10 @@ const ChatWrapper = () => {
appData={appData}
config={appConfig}
chatList={messageList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
isResponding={respondingState}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[768px] ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile ? 'px-2' : 'px-4'}`}
chatFooterInnerClassName={`mx-auto w-full max-w-[768px] ${isMobile ? 'px-2' : 'px-4'}`}
onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms}
@ -227,6 +260,7 @@ const ChatWrapper = () => {
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
inputDisabled={inputDisabled}
isMobile={isMobile}
sidebarCollapseState={sidebarCollapseState}
/>
</div>
)

View File

@ -50,6 +50,10 @@ export type ChatWithHistoryContextValue = {
themeBuilder?: ThemeBuilder
sidebarCollapseState?: boolean
handleSidebarCollapse: (state: boolean) => void
clearChatList?: boolean
setClearChatList: (state: boolean) => void
isResponding?: boolean
setIsResponding: (state: boolean) => void,
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
@ -77,5 +81,9 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
currentChatInstanceRef: { current: { handleStop: () => {} } },
sidebarCollapseState: false,
handleSidebarCollapse: () => {},
clearChatList: false,
setClearChatList: () => {},
isResponding: false,
setIsResponding: () => {},
})
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

View File

@ -9,7 +9,7 @@ import {
useChatWithHistoryContext,
} from '../context'
import Operation from './operation'
import ActionButton from '@/app/components/base/action-button'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
@ -33,6 +33,7 @@ const Header = () => {
handleNewConversation,
sidebarCollapseState,
handleSidebarCollapse,
isResponding,
} = useChatWithHistoryContext()
const { t } = useTranslation()
const isSidebarCollapsed = sidebarCollapseState
@ -106,9 +107,21 @@ const Header = () => {
<div className='h-[14px] w-px bg-divider-regular'></div>
</div>
{isSidebarCollapsed && (
<ActionButton size='l' onClick={handleNewConversation}>
<Tooltip
disabled={!!currentConversationId}
popupContent={t('share.chat.newChatTip')}
>
<div>
<ActionButton
size='l'
state={(!currentConversationId || isResponding) ? ActionButtonState.Disabled : ActionButtonState.Default}
disabled={!currentConversationId || isResponding}
onClick={handleNewConversation}
>
<RiEditBoxLine className='w-[18px] h-[18px]' />
</ActionButton>
</div>
</Tooltip>
)}
</div>
<div className='flex items-center gap-1'>

View File

@ -150,6 +150,8 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const [clearChatList, setClearChatList] = useState(false)
const [isResponding, setIsResponding] = useState(false)
const appPrevChatTree = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? buildChatItemTree(getFormattedChatList(appChatListData.data))
@ -310,20 +312,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
handleConversationIdInfoChange(conversationId)
}, [handleConversationIdInfoChange])
if (conversationId)
setClearChatList(false)
}, [handleConversationIdInfoChange, setClearChatList])
const handleNewConversation = useCallback(() => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
if (showNewConversationItemInList) {
handleChangeConversation('')
}
else if (currentConversationId) {
handleConversationIdInfoChange('')
setShowNewConversationItemInList(true)
handleChangeConversation('')
handleNewConversationInputsChange({})
}
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
setClearChatList(true)
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
const handleUpdateConversationList = useCallback(() => {
mutateAppConversationData()
mutateAppPinnedConversationData()
@ -462,5 +460,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
currentChatInstanceRef,
sidebarCollapseState,
handleSidebarCollapse,
clearChatList,
setClearChatList,
isResponding,
setIsResponding,
}
}

View File

@ -82,7 +82,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
{isMobile && (
<HeaderInMobile />
)}
<div className={cn('relative grow p-2')}>
<div className={cn('relative grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}>
{isSidebarCollapsed && (
<div
className={cn(
@ -95,7 +95,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<Sidebar isPanel />
</div>
)}
<div className='h-full flex flex-col bg-chatbot-bg rounded-2xl border-[0,5px] border-components-panel-border-subtle overflow-hidden'>
<div className={cn('h-full flex flex-col bg-chatbot-bg border-[0,5px] border-components-panel-border-subtle overflow-hidden', isMobile ? 'rounded-t-2xl' : 'rounded-2xl')}>
{!isMobile && <Header />}
{appChatListDataLoading && (
<Loading type='app' />
@ -153,6 +153,10 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
currentChatInstanceRef,
sidebarCollapseState,
handleSidebarCollapse,
clearChatList,
setClearChatList,
isResponding,
setIsResponding,
} = useChatWithHistory(installedAppInfo)
return (
@ -190,6 +194,10 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
themeBuilder,
sidebarCollapseState,
handleSidebarCollapse,
clearChatList,
setClearChatList,
isResponding,
setIsResponding,
}}>
<ChatWithHistory className={className} />
</ChatWithHistoryContext.Provider>

View File

@ -41,6 +41,7 @@ const Sidebar = ({ isPanel }: Props) => {
sidebarCollapseState,
handleSidebarCollapse,
isMobile,
isResponding,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
@ -105,7 +106,7 @@ const Sidebar = ({ isPanel }: Props) => {
)}
</div>
<div className='shrink-0 px-3 py-4'>
<Button variant='secondary-accent' className='w-full justify-center' onClick={handleNewConversation}>
<Button variant='secondary-accent' disabled={isResponding} className='w-full justify-center' onClick={handleNewConversation}>
<RiEditBoxLine className='w-4 h-4 mr-1' />
{t('share.chat.newChat')}
</Button>

View File

@ -110,7 +110,7 @@ const Answer: FC<AnswerProps> = ({
</div>
)}
</div>
<div className='chat-answer-container group grow w-0 ml-4' ref={containerRef}>
<div className='chat-answer-container group grow w-0 ml-4 pb-4' ref={containerRef}>
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
<div
ref={contentRef}

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import {
RiClipboardLine,
RiEditLine,
RiReplay15Line,
RiResetLeftLine,
RiThumbDownLine,
RiThumbUpLine,
} from '@remixicon/react'
@ -130,7 +130,7 @@ const Operation: FC<OperationProps> = ({
</ActionButton>
{!noChatInput && (
<ActionButton onClick={() => onRegenerate?.(item)}>
<RiReplay15Line className='w-4 h-4' />
<RiResetLeftLine className='w-4 h-4' />
</ActionButton>
)}
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (

View File

@ -51,6 +51,8 @@ export const useChat = (
},
prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void,
clearChatList?: boolean,
clearChatListCallback?: (state: boolean) => void,
) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
@ -90,7 +92,7 @@ export const useChat = (
}
else {
ret.unshift({
id: `${Date.now()}`,
id: 'opening-statement',
content: getIntroduction(config.opening_statement),
isAnswer: true,
isOpeningStatement: true,
@ -163,12 +165,13 @@ export const useChat = (
suggestedQuestionsAbortControllerRef.current.abort()
}, [stopChat, handleResponding])
const handleRestart = useCallback(() => {
const handleRestart = useCallback((cb?: any) => {
conversationId.current = ''
taskIdRef.current = ''
handleStop()
setChatTree([])
setSuggestQuestions([])
cb?.()
}, [handleStop])
const updateCurrentQAOnTree = useCallback(({
@ -682,6 +685,11 @@ export const useChat = (
})
}, [chatList, updateChatTreeNode])
useEffect(() => {
if (clearChatList)
handleRestart(() => clearChatListCallback?.(false))
}, [clearChatList, clearChatListCallback, handleRestart])
return {
chatList,
setTargetMessageId,

View File

@ -72,6 +72,7 @@ export type ChatProps = {
noSpacing?: boolean
inputDisabled?: boolean
isMobile?: boolean
sidebarCollapseState?: boolean
}
const Chat: FC<ChatProps> = ({
@ -110,6 +111,7 @@ const Chat: FC<ChatProps> = ({
noSpacing,
inputDisabled,
isMobile,
sidebarCollapseState,
}) => {
const { t } = useTranslation()
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
@ -193,6 +195,11 @@ const Chat: FC<ChatProps> = ({
}
}, [])
useEffect(() => {
if (!sidebarCollapseState)
setTimeout(() => handleWindowResize(), 200)
}, [sidebarCollapseState])
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
return (
@ -255,7 +262,7 @@ const Chat: FC<ChatProps> = ({
</div>
</div>
<div
className={`absolute bottom-0 bg-chat-input-mask ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
className={`absolute bottom-0 bg-chat-input-mask flex justify-center ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
ref={chatFooterRef}
>
<div

View File

@ -21,6 +21,8 @@ import {
import AppIcon from '@/app/components/base/app-icon'
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames'
const ChatWrapper = () => {
@ -41,6 +43,9 @@ const ChatWrapper = () => {
handleFeedback,
currentChatInstanceRef,
themeBuilder,
clearChatList,
setClearChatList,
setIsResponding,
} = useEmbeddedChatbotContext()
const appConfig = useMemo(() => {
const config = appParams || {}
@ -60,7 +65,7 @@ const ChatWrapper = () => {
setTargetMessageId,
handleSend,
handleStop,
isResponding,
isResponding: respondingState,
suggestedQuestions,
} = useChat(
appConfig,
@ -70,6 +75,8 @@ const ChatWrapper = () => {
},
appPrevChatList,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
clearChatList,
setClearChatList,
)
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
const inputDisabled = useMemo(() => {
@ -108,6 +115,9 @@ const ChatWrapper = () => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
}, [currentChatInstanceRef, handleStop])
useEffect(() => {
setIsResponding(respondingState)
}, [respondingState, setIsResponding])
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
const data: any = {
@ -167,12 +177,33 @@ const ChatWrapper = () => {
const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
if (respondingState)
return null
if (currentConversationId)
return null
if (!welcomeMessage)
return null
if (!collapsed && inputsForms.length > 0)
return null
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return (
<div className='h-[50vh] py-12 px-4 flex items-center justify-center'>
<div className='grow max-w-[720px] flex gap-4'>
<AppIcon
size='xl'
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='grow px-4 py-3 bg-chat-bubble-bg text-text-primary rounded-2xl body-lg-regular'>
<Markdown content={welcomeMessage.content} />
<SuggestedQuestions item={welcomeMessage} />
</div>
</div>
</div>
)
}
return (
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
<AppIcon
@ -182,10 +213,10 @@ const ChatWrapper = () => {
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
<Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} />
</div>
)
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
const answerIcon = isDify()
? <LogoAvatar className='relative shrink-0' />
@ -203,10 +234,10 @@ const ChatWrapper = () => {
appData={appData}
config={appConfig}
chatList={messageList}
isResponding={isResponding}
chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
isResponding={respondingState}
chatContainerInnerClassName={cn('mx-auto w-full max-w-full pt-4 tablet:px-4', isMobile && 'px-4')}
chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')}
chatFooterInnerClassName={cn('mx-auto w-full max-w-full px-4', isMobile && 'px-2')}
onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms}

View File

@ -42,6 +42,10 @@ export type EmbeddedChatbotContextValue = {
handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
themeBuilder?: ThemeBuilder
clearChatList?: boolean
setClearChatList: (state: boolean) => void
isResponding?: boolean
setIsResponding: (state: boolean) => void,
}
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
@ -62,5 +66,9 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
isInstalledApp: false,
handleFeedback: () => {},
currentChatInstanceRef: { current: { handleStop: () => {} } },
clearChatList: false,
setClearChatList: () => {},
isResponding: false,
setIsResponding: () => {},
})
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)

View File

@ -103,6 +103,8 @@ export const useEmbeddedChatbot = () => {
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const [clearChatList, setClearChatList] = useState(false)
const [isResponding, setIsResponding] = useState(false)
const appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? buildChatItemTree(getFormattedChatList(appChatListData.data))
@ -283,20 +285,16 @@ export const useEmbeddedChatbot = () => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
handleConversationIdInfoChange(conversationId)
}, [handleConversationIdInfoChange])
if (conversationId)
setClearChatList(false)
}, [handleConversationIdInfoChange, setClearChatList])
const handleNewConversation = useCallback(() => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
if (showNewConversationItemInList) {
handleChangeConversation('')
}
else if (currentConversationId) {
handleConversationIdInfoChange('')
setShowNewConversationItemInList(true)
handleChangeConversation('')
handleNewConversationInputsChange({})
}
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
setClearChatList(true)
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
setNewConversationId(newConversationId)
@ -342,5 +340,9 @@ export const useEmbeddedChatbot = () => {
chatShouldReloadKey,
handleFeedback,
currentChatInstanceRef,
clearChatList,
setClearChatList,
isResponding,
setIsResponding,
}
}

View File

@ -156,6 +156,10 @@ const EmbeddedChatbotWrapper = () => {
appId,
handleFeedback,
currentChatInstanceRef,
clearChatList,
setClearChatList,
isResponding,
setIsResponding,
} = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{
@ -185,6 +189,10 @@ const EmbeddedChatbotWrapper = () => {
handleFeedback,
currentChatInstanceRef,
themeBuilder,
clearChatList,
setClearChatList,
isResponding,
setIsResponding,
}}>
<Chatbot />
</EmbeddedChatbotContext.Provider>

View File

@ -213,7 +213,7 @@
display: block;
width: max-content;
max-width: 100%;
overflow: hidden;
overflow: auto;
border: 1px solid var(--color-divider-regular);
border-radius: 8px;
}

View File

@ -6,6 +6,7 @@ const translation = {
},
chat: {
newChat: 'Start New chat',
newChatTip: 'Already in a new chat',
chatSettingsTitle: 'New chat setup',
chatFormTip: 'Chat settings cannot be modified after the chat has started.',
pinnedTitle: 'Pinned',

View File

@ -6,6 +6,7 @@ const translation = {
},
chat: {
newChat: '开启新对话',
newChatTip: '已在新对话中',
chatSettingsTitle: '新对话设置',
chatFormTip: '对话开始后,对话设置将无法修改。',
pinnedTitle: '已置顶',