dify
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
RiEditBoxLine,
|
||||
RiLayoutRight2Line,
|
||||
RiResetLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useChatWithHistoryContext,
|
||||
} from '../context'
|
||||
import Operation from './operation'
|
||||
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'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Header = () => {
|
||||
const {
|
||||
appData,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
inputsForms,
|
||||
pinnedConversationList,
|
||||
handlePinConversation,
|
||||
handleUnpinConversation,
|
||||
conversationRenaming,
|
||||
handleRenameConversation,
|
||||
handleDeleteConversation,
|
||||
handleNewConversation,
|
||||
sidebarCollapseState,
|
||||
handleSidebarCollapse,
|
||||
isResponding,
|
||||
} = useChatWithHistoryContext()
|
||||
const { t } = useTranslation()
|
||||
const isSidebarCollapsed = sidebarCollapseState
|
||||
|
||||
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
|
||||
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
|
||||
const handleOperate = useCallback((type: string) => {
|
||||
if (type === 'pin')
|
||||
handlePinConversation(currentConversationId)
|
||||
|
||||
if (type === 'unpin')
|
||||
handleUnpinConversation(currentConversationId)
|
||||
|
||||
if (type === 'delete')
|
||||
setShowConfirm(currentConversationItem as any)
|
||||
|
||||
if (type === 'rename')
|
||||
setShowRename(currentConversationItem as any)
|
||||
}, [currentConversationId, currentConversationItem, 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='flex h-14 shrink-0 items-center justify-between p-3'>
|
||||
<div className={cn('flex items-center gap-1 transition-all duration-200 ease-in-out', !isSidebarCollapsed && 'user-select-none opacity-0')}>
|
||||
<ActionButton className={cn(!isSidebarCollapsed && 'cursor-default')} size='l' onClick={() => handleSidebarCollapse(false)}>
|
||||
<RiLayoutRight2Line className='h-[18px] w-[18px]' />
|
||||
</ActionButton>
|
||||
<div className='mr-1 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>
|
||||
{!currentConversationId && (
|
||||
<div className={cn('system-md-semibold grow truncate text-text-secondary')}>{appData?.site.title}</div>
|
||||
)}
|
||||
{currentConversationId && currentConversationItem && isSidebarCollapsed && (
|
||||
<>
|
||||
<div className='p-1 text-divider-deep'>/</div>
|
||||
<Operation
|
||||
title={currentConversationItem?.name || ''}
|
||||
isPinned={!!isPin}
|
||||
togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
|
||||
isShowDelete
|
||||
isShowRenameConversation
|
||||
onRenameConversation={() => handleOperate('rename')}
|
||||
onDelete={() => handleOperate('delete')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className='flex items-center px-1'>
|
||||
<div className='h-[14px] w-px bg-divider-regular'></div>
|
||||
</div>
|
||||
{isSidebarCollapsed && (
|
||||
<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='h-[18px] w-[18px]' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={handleNewConversation}>
|
||||
<RiResetLeftLine className='h-[18px] w-[18px]' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
content={t('share.chat.deleteConversation.content') || ''}
|
||||
isShow
|
||||
onCancel={handleCancelConfirm}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
{showRename && (
|
||||
<RenameModal
|
||||
isShow
|
||||
onClose={handleCancelRename}
|
||||
saveLoading={conversationRenaming}
|
||||
name={showRename?.name || ''}
|
||||
onSave={handleRename}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
|
||||
type Props = {
|
||||
handleResetChat: () => void
|
||||
handleViewChatSettings: () => void
|
||||
hideViewChatSettings?: boolean
|
||||
}
|
||||
|
||||
const MobileOperationDropdown = ({
|
||||
handleResetChat,
|
||||
handleViewChatSettings,
|
||||
hideViewChatSettings = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
|
||||
<RiMoreFill className='h-[18px] w-[18px]' />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-40">
|
||||
<div
|
||||
className={'min-w-[160px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm'}
|
||||
>
|
||||
<div className='system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={handleResetChat}>
|
||||
<span className='grow'>{t('share.chat.resetChat')}</span>
|
||||
</div>
|
||||
{!hideViewChatSettings && (
|
||||
<div className='system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' onClick={handleViewChatSettings}>
|
||||
<span className='grow'>{t('share.chat.viewChatSettings')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileOperationDropdown
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
isPinned: boolean
|
||||
isShowRenameConversation?: boolean
|
||||
onRenameConversation?: () => void
|
||||
isShowDelete: boolean
|
||||
togglePin: () => void
|
||||
onDelete: () => void
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
const Operation: FC<Props> = ({
|
||||
title,
|
||||
isPinned,
|
||||
togglePin,
|
||||
isShowRenameConversation,
|
||||
onRenameConversation,
|
||||
isShowDelete,
|
||||
onDelete,
|
||||
placement = 'bottom-start',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement={placement}
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<div className={cn('flex cursor-pointer items-center rounded-lg p-1.5 pl-2 text-text-secondary hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
|
||||
<div className='system-md-semibold'>{title}</div>
|
||||
<RiArrowDownSLine className='h-4 w-4 ' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div
|
||||
className={'min-w-[120px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm'}
|
||||
>
|
||||
<div className={cn('system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover')} onClick={togglePin}>
|
||||
<span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
|
||||
</div>
|
||||
{isShowRenameConversation && (
|
||||
<div className={cn('system-md-regular flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover')} onClick={onRenameConversation}>
|
||||
<span className='grow'>{t('explore.sidebar.action.rename')}</span>
|
||||
</div>
|
||||
)}
|
||||
{isShowDelete && (
|
||||
<div className={cn('system-md-regular group flex cursor-pointer items-center space-x-1 rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
|
||||
<span className='grow'>{t('explore.sidebar.action.delete')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(Operation)
|
||||
Reference in New Issue
Block a user