mirror of https://github.com/langgenius/dify.git
add comment preview
This commit is contained in:
parent
4188c9a1dd
commit
86a9a51952
|
|
@ -1,19 +1,27 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import CommentPreview from './comment-preview'
|
||||
import type { WorkflowCommentList } from '@/service/workflow-comment'
|
||||
|
||||
type CommentIconProps = {
|
||||
comment: WorkflowCommentList
|
||||
onClick: () => void
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) => {
|
||||
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isActive = false }) => {
|
||||
const { flowToScreenPosition } = useReactFlow()
|
||||
const viewport = useViewport()
|
||||
const [showPreview, setShowPreview] = useState(false)
|
||||
|
||||
const handlePreviewClick = () => {
|
||||
setShowPreview(false)
|
||||
onClick()
|
||||
}
|
||||
|
||||
const screenPosition = useMemo(() => {
|
||||
return flowToScreenPosition({
|
||||
|
|
@ -35,30 +43,58 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) =>
|
|||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute z-10 cursor-pointer"
|
||||
style={{
|
||||
left: screenPosition.x,
|
||||
top: screenPosition.y,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'}
|
||||
style={{ width: dynamicWidth }}
|
||||
className="absolute z-10"
|
||||
style={{
|
||||
left: screenPosition.x,
|
||||
top: screenPosition.y,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-[6px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full border border-components-panel-border bg-components-panel-bg">
|
||||
<div className="flex h-full w-full items-center justify-center px-1">
|
||||
<UserAvatarList
|
||||
users={comment.participants}
|
||||
maxVisible={3}
|
||||
size={24}
|
||||
/>
|
||||
<div
|
||||
className={isActive ? '' : 'cursor-pointer'}
|
||||
onClick={isActive ? undefined : onClick}
|
||||
onMouseEnter={isActive ? undefined : () => setShowPreview(true)}
|
||||
onMouseLeave={isActive ? undefined : () => setShowPreview(false)}
|
||||
>
|
||||
<div
|
||||
className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'}
|
||||
style={{ width: dynamicWidth }}
|
||||
>
|
||||
<div className={`absolute inset-[6px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full border ${
|
||||
isActive
|
||||
? 'border-2 border-primary-500 bg-components-panel-bg'
|
||||
: 'border-components-panel-border bg-components-panel-bg'
|
||||
}`}>
|
||||
<div className="flex h-full w-full items-center justify-center px-1">
|
||||
<UserAvatarList
|
||||
users={comment.participants}
|
||||
maxVisible={3}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview panel */}
|
||||
{showPreview && !isActive && (
|
||||
<div
|
||||
className="absolute z-20"
|
||||
style={{
|
||||
left: screenPosition.x - dynamicWidth / 2,
|
||||
top: screenPosition.y + 20,
|
||||
transform: 'translateY(-100%)',
|
||||
}}
|
||||
onMouseEnter={() => setShowPreview(true)}
|
||||
onMouseLeave={() => setShowPreview(false)}
|
||||
>
|
||||
<CommentPreview comment={comment} onClick={handlePreviewClick} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}, (prevProps, nextProps) => {
|
||||
return (
|
||||
|
|
@ -66,6 +102,7 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) =>
|
|||
&& prevProps.comment.position_x === nextProps.comment.position_x
|
||||
&& prevProps.comment.position_y === nextProps.comment.position_y
|
||||
&& prevProps.onClick === nextProps.onClick
|
||||
&& prevProps.isActive === nextProps.isActive
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import type { WorkflowCommentList } from '@/service/workflow-comment'
|
||||
import { useFormatTimeFromNow } from '@/app/components/workflow/hooks'
|
||||
|
||||
type CommentPreviewProps = {
|
||||
comment: WorkflowCommentList
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const CommentPreview: FC<CommentPreviewProps> = ({ comment, onClick }) => {
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-80 cursor-pointer rounded-br-xl rounded-tl-xl rounded-tr-xl border border-components-panel-border bg-components-panel-bg p-4 shadow-lg transition-colors hover:bg-components-panel-on-panel-item-bg-hover"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<UserAvatarList
|
||||
users={comment.participants}
|
||||
maxVisible={3}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-start">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="system-sm-medium truncate text-text-primary">{comment.created_by_account.name}</div>
|
||||
<div className="system-2xs-regular shrink-0 text-text-tertiary">
|
||||
{formatTimeFromNow(comment.updated_at * 1000)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="system-sm-regular break-words text-text-secondary">{comment.content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CommentPreview)
|
||||
|
|
@ -216,9 +216,9 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
<div
|
||||
className='absolute z-50 w-[360px] max-w-[360px]'
|
||||
style={{
|
||||
left: screenPosition.x,
|
||||
left: screenPosition.x + 40,
|
||||
top: screenPosition.y,
|
||||
transform: 'translate(-50%, -100%) translateY(-24px)',
|
||||
transform: 'translateY(-20%)',
|
||||
}}
|
||||
>
|
||||
<div className='relative flex h-[360px] flex-col overflow-hidden rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'>
|
||||
|
|
|
|||
|
|
@ -454,21 +454,29 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
const canGoPrev = index > 0
|
||||
const canGoNext = index < comments.length - 1
|
||||
return (
|
||||
<CommentThread
|
||||
key={comment.id}
|
||||
comment={activeComment}
|
||||
loading={activeCommentLoading}
|
||||
onClose={handleActiveCommentClose}
|
||||
onResolve={() => handleCommentResolve(comment.id)}
|
||||
onDelete={() => setPendingDeleteCommentId(comment.id)}
|
||||
onPrev={canGoPrev ? () => handleCommentNavigate('prev') : undefined}
|
||||
onNext={canGoNext ? () => handleCommentNavigate('next') : undefined}
|
||||
onReply={(content, ids) => handleCommentReply(comment.id, content, ids ?? [])}
|
||||
onReplyEdit={(replyId, content, ids) => handleCommentReplyUpdate(comment.id, replyId, content, ids ?? [])}
|
||||
onReplyDelete={replyId => setPendingDeleteReply({ commentId: comment.id, replyId })}
|
||||
canGoPrev={canGoPrev}
|
||||
canGoNext={canGoNext}
|
||||
/>
|
||||
<>
|
||||
<CommentIcon
|
||||
key={`${comment.id}-icon`}
|
||||
comment={comment}
|
||||
onClick={() => handleCommentIconClick(comment)}
|
||||
isActive={true}
|
||||
/>
|
||||
<CommentThread
|
||||
key={`${comment.id}-thread`}
|
||||
comment={activeComment}
|
||||
loading={activeCommentLoading}
|
||||
onClose={handleActiveCommentClose}
|
||||
onResolve={() => handleCommentResolve(comment.id)}
|
||||
onDelete={() => setPendingDeleteCommentId(comment.id)}
|
||||
onPrev={canGoPrev ? () => handleCommentNavigate('prev') : undefined}
|
||||
onNext={canGoNext ? () => handleCommentNavigate('next') : undefined}
|
||||
onReply={(content, ids) => handleCommentReply(comment.id, content, ids ?? [])}
|
||||
onReplyEdit={(replyId, content, ids) => handleCommentReplyUpdate(comment.id, replyId, content, ids ?? [])}
|
||||
onReplyDelete={replyId => setPendingDeleteReply({ commentId: comment.id, replyId })}
|
||||
canGoPrev={canGoPrev}
|
||||
canGoNext={canGoNext}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue