dify/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
Stephen Zhou 6d0e36479b
refactor(i18n): use JSON with flattened key and namespace (#30114)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-12-29 14:52:32 +08:00

192 lines
6.8 KiB
TypeScript

import type { ConversationItem } from '@/models/share'
import {
RiEditBoxLine,
RiExpandRightLine,
RiLayoutLeft2Line,
} from '@remixicon/react'
import {
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import Confirm from '@/app/components/base/confirm'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'
import { useChatWithHistoryContext } from '../context'
type Props = {
isPanel?: boolean
panelVisible?: boolean
}
const Sidebar = ({ isPanel, panelVisible }: Props) => {
const { t } = useTranslation()
const {
isInstalledApp,
appData,
handleNewConversation,
pinnedConversationList,
conversationList,
currentConversationId,
handleChangeConversation,
handlePinConversation,
handleUnpinConversation,
conversationRenaming,
handleRenameConversation,
handleDeleteConversation,
sidebarCollapseState,
handleSidebarCollapse,
isMobile,
isResponding,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
const handleOperate = useCallback((type: string, item: ConversationItem) => {
if (type === 'pin')
handlePinConversation(item.id)
if (type === 'unpin')
handleUnpinConversation(item.id)
if (type === 'delete')
setShowConfirm(item)
if (type === 'rename')
setShowRename(item)
}, [handlePinConversation, handleUnpinConversation])
const handleCancelConfirm = useCallback(() => {
setShowConfirm(null)
}, [])
const handleDelete = useCallback(() => {
if (showConfirm)
handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
}, [showConfirm, handleDeleteConversation, handleCancelConfirm])
const handleCancelRename = useCallback(() => {
setShowRename(null)
}, [])
const handleRename = useCallback((newName: string) => {
if (showRename)
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
}, [showRename, handleRenameConversation, handleCancelRename])
return (
<div className={cn(
'flex w-full grow flex-col',
isPanel && 'rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-bg shadow-lg',
)}
>
<div className={cn(
'flex shrink-0 items-center gap-3 p-3 pr-2',
)}
>
<div className="shrink-0">
<AppIcon
size="large"
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
</div>
<div className={cn('system-md-semibold grow truncate text-text-secondary')}>{appData?.site.title}</div>
{!isMobile && isSidebarCollapsed && (
<ActionButton size="l" onClick={() => handleSidebarCollapse(false)}>
<RiExpandRightLine className="h-[18px] w-[18px]" />
</ActionButton>
)}
{!isMobile && !isSidebarCollapsed && (
<ActionButton size="l" onClick={() => handleSidebarCollapse(true)}>
<RiLayoutLeft2Line className="h-[18px] w-[18px]" />
</ActionButton>
)}
</div>
<div className="shrink-0 px-3 py-4">
<Button variant="secondary-accent" disabled={isResponding} className="w-full justify-center" onClick={handleNewConversation}>
<RiEditBoxLine className="mr-1 h-4 w-4" />
{t('chat.newChat', { ns: 'share' })}
</Button>
</div>
<div className="h-0 grow space-y-2 overflow-y-auto px-3 pt-4">
{/* pinned list */}
{!!pinnedConversationList.length && (
<div className="mb-4">
<List
isPin
title={t('chat.pinnedTitle', { ns: 'share' }) || ''}
list={pinnedConversationList}
onChangeConversation={handleChangeConversation}
onOperate={handleOperate}
currentConversationId={currentConversationId}
/>
</div>
)}
{!!conversationList.length && (
<List
title={(pinnedConversationList.length && t('chat.unpinnedTitle', { ns: 'share' })) || ''}
list={conversationList}
onChangeConversation={handleChangeConversation}
onOperate={handleOperate}
currentConversationId={currentConversationId}
/>
)}
</div>
<div className="flex shrink-0 items-center justify-between p-3">
<MenuDropdown
hideLogout={isInstalledApp}
placement="top-start"
data={appData?.site}
forceClose={isPanel && !panelVisible}
/>
{/* powered by */}
<div className="shrink-0">
{!appData?.custom_config?.remove_webapp_brand && (
<div className={cn(
'flex shrink-0 items-center gap-1.5 px-1',
)}
>
<div className="system-2xs-medium-uppercase text-text-tertiary">{t('chat.poweredBy', { ns: 'share' })}</div>
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />
: appData?.custom_config?.replace_webapp_logo
? <img src={`${appData?.custom_config?.replace_webapp_logo}`} alt="logo" className="block h-5 w-auto" />
: <DifyLogo size="small" />
}
</div>
)}
</div>
{!!showConfirm && (
<Confirm
title={t('chat.deleteConversation.title', { ns: 'share' })}
content={t('chat.deleteConversation.content', { ns: 'share' }) || ''}
isShow
onCancel={handleCancelConfirm}
onConfirm={handleDelete}
/>
)}
{showRename && (
<RenameModal
isShow
onClose={handleCancelRename}
saveLoading={conversationRenaming}
name={showRename?.name || ''}
onSave={handleRename}
/>
)}
</div>
</div>
)
}
export default Sidebar