mirror of https://github.com/langgenius/dify.git
Merge branch 'p284' into deploy/dev
This commit is contained in:
commit
96c7c86e9d
|
|
@ -1044,7 +1044,7 @@ class WorkflowOnlineUsersApi(Resource):
|
|||
|
||||
workflow_ids = [id.strip() for id in args["workflow_ids"].split(",")]
|
||||
|
||||
results = {}
|
||||
results = []
|
||||
for workflow_id in workflow_ids:
|
||||
users_json = redis_client.hgetall(f"workflow_online_users:{workflow_id}")
|
||||
|
||||
|
|
@ -1054,7 +1054,7 @@ class WorkflowOnlineUsersApi(Resource):
|
|||
users.append(json.loads(user_info_json))
|
||||
except Exception:
|
||||
continue
|
||||
results[workflow_id] = users
|
||||
results.append({"workflow_id": workflow_id, "users": users})
|
||||
|
||||
return {"data": results}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from flask_restx import fields
|
||||
|
||||
online_user_partial_fields = {
|
||||
"id": fields.String,
|
||||
"user_id": fields.String,
|
||||
"username": fields.String,
|
||||
"avatar": fields.String,
|
||||
"sid": fields.String,
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ class WorkflowService:
|
|||
|
||||
workflow.environment_variables = environment_variables
|
||||
workflow.updated_by = account.id
|
||||
workflow.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
workflow.updated_at = naive_utc_now()
|
||||
|
||||
# commit db session changes
|
||||
db.session.commit()
|
||||
|
|
@ -285,7 +285,7 @@ class WorkflowService:
|
|||
|
||||
workflow.conversation_variables = conversation_variables
|
||||
workflow.updated_by = account.id
|
||||
workflow.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
workflow.updated_at = naive_utc_now()
|
||||
|
||||
# commit db session changes
|
||||
db.session.commit()
|
||||
|
|
@ -311,7 +311,7 @@ class WorkflowService:
|
|||
|
||||
workflow.features = json.dumps(features)
|
||||
workflow.updated_by = account.id
|
||||
workflow.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
workflow.updated_at = naive_utc_now()
|
||||
|
||||
# commit db session changes
|
||||
db.session.commit()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||
import { formatTime } from '@/utils/time'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import type { WorkflowOnlineUser } from '@/models/app'
|
||||
|
||||
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
|
||||
ssr: false,
|
||||
|
|
@ -55,9 +57,10 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont
|
|||
export type AppCardProps = {
|
||||
app: App
|
||||
onRefresh?: () => void
|
||||
onlineUsers?: WorkflowOnlineUser[]
|
||||
}
|
||||
|
||||
const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
|
@ -331,6 +334,19 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
|
||||
}, [app.updated_at, app.created_at])
|
||||
|
||||
const onlineUserAvatars = useMemo(() => {
|
||||
if (!onlineUsers.length)
|
||||
return []
|
||||
|
||||
return onlineUsers
|
||||
.map(user => ({
|
||||
id: user.user_id || user.sid || '',
|
||||
name: user.username || 'User',
|
||||
avatar_url: user.avatar || undefined,
|
||||
}))
|
||||
.filter(user => !!user.id)
|
||||
}, [onlineUsers])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -375,6 +391,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
<RiVerifiedBadgeLine className='h-4 w-4 text-text-quaternary' />
|
||||
</Tooltip>}
|
||||
</div>
|
||||
<div>
|
||||
{onlineUserAvatars.length > 0 && (
|
||||
<UserAvatarList users={onlineUserAvatars} maxVisible={3} size={20} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
useRouter,
|
||||
} from 'next/navigation'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import {
|
||||
|
|
@ -19,8 +20,8 @@ import AppCard from './app-card'
|
|||
import NewAppCard from './new-app-card'
|
||||
import useAppsQueryState from './hooks/use-apps-query-state'
|
||||
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import type { AppListResponse, WorkflowOnlineUser } from '@/models/app'
|
||||
import { fetchAppList, fetchWorkflowOnlineUsers } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
|
|
@ -112,6 +113,37 @@ const List = () => {
|
|||
},
|
||||
)
|
||||
|
||||
const apps = useMemo(() => data?.flatMap(page => page.data) ?? [], [data])
|
||||
|
||||
const workflowIds = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
apps.forEach((appItem) => {
|
||||
const workflowId = appItem.id
|
||||
if (!workflowId)
|
||||
return
|
||||
|
||||
if (appItem.mode === 'workflow' || appItem.mode === 'advanced-chat')
|
||||
ids.add(workflowId)
|
||||
})
|
||||
return Array.from(ids)
|
||||
}, [apps])
|
||||
|
||||
const { data: onlineUsersByWorkflow, mutate: refreshOnlineUsers } = useSWR<Record<string, WorkflowOnlineUser[]>>(
|
||||
workflowIds.length ? { workflowIds } : null,
|
||||
fetchWorkflowOnlineUsers,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!workflowIds.length)
|
||||
return
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
refreshOnlineUsers()
|
||||
}, 10000)
|
||||
|
||||
return () => window.clearInterval(timer)
|
||||
}, [workflowIds.join(','), refreshOnlineUsers])
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||
|
|
@ -213,7 +245,12 @@ const List = () => {
|
|||
{isCurrentWorkspaceEditor
|
||||
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
|
||||
{data.map(({ data: apps }) => apps.map(app => (
|
||||
<AppCard key={app.id} app={app} onRefresh={mutate} />
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
onRefresh={mutate}
|
||||
onlineUsers={onlineUsersByWorkflow?.[app.id] ?? []}
|
||||
/>
|
||||
)))}
|
||||
</div>
|
||||
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12" fill="none">
|
||||
<path d="M12.3334 4C12.3334 2.52725 11.1395 1.33333 9.66671 1.33333H4.33337C2.86062 1.33333 1.66671 2.52724 1.66671 4V10.6667H9.66671C11.1395 10.6667 12.3334 9.47274 12.3334 8V4ZM7.66671 6.66667V8H4.33337V6.66667H7.66671ZM9.66671 4V5.33333H4.33337V4H9.66671ZM13.6667 8C13.6667 10.2091 11.8758 12 9.66671 12H0.333374V4C0.333374 1.79086 2.12424 0 4.33337 0H9.66671C11.8758 0 13.6667 1.79086 13.6667 4V8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"xmlns": "http://www.w3.org/2000/svg",
|
||||
"width": "14",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 14 12",
|
||||
"fill": "none"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12.3334 4C12.3334 2.52725 11.1395 1.33333 9.66671 1.33333H4.33337C2.86062 1.33333 1.66671 2.52724 1.66671 4V10.6667H9.66671C11.1395 10.6667 12.3334 9.47274 12.3334 8V4ZM7.66671 6.66667V8H4.33337V6.66667H7.66671ZM9.66671 4V5.33333H4.33337V4H9.66671ZM13.6667 8C13.6667 10.2091 11.8758 12 9.66671 12H0.333374V4C0.333374 1.79086 2.12424 0 4.33337 0H9.66671C11.8758 0 13.6667 1.79086 13.6667 4V8Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Comment"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Comment.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'Comment'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export { default as Icon3Dots } from './Icon3Dots'
|
||||
export { default as Comment } from './Comment'
|
||||
export { default as DefaultToolIcon } from './DefaultToolIcon'
|
||||
export { default as Message3Fill } from './Message3Fill'
|
||||
export { default as RowStruct } from './RowStruct'
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
|
|||
name={user.name}
|
||||
avatar={user.avatar_url || null}
|
||||
size={size}
|
||||
className='ring-2 ring-white'
|
||||
className='ring-2 ring-components-panel-bg'
|
||||
backgroundColor={userColor}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -60,7 +60,7 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
|
|||
)}
|
||||
{shouldShowCount && remainingCount > 0 && (
|
||||
<div
|
||||
className={'flex items-center justify-center rounded-full bg-components-panel-on-panel-item-bg text-[10px] leading-none text-text-secondary ring-2 ring-white'}
|
||||
className={'flex items-center justify-center rounded-full bg-gray-500 text-[10px] leading-none text-white ring-2 ring-components-panel-bg'}
|
||||
style={{
|
||||
zIndex: 0,
|
||||
width: size,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
|||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import WorkflowChildren from './workflow-children'
|
||||
import UserCursors from '@/app/components/workflow/collaboration/components/user-cursors'
|
||||
|
||||
import {
|
||||
useAvailableNodesMetaData,
|
||||
|
|
@ -46,7 +45,7 @@ const WorkflowMain = ({
|
|||
const reactFlow = useReactFlow()
|
||||
|
||||
const store = useStoreApi()
|
||||
const { startCursorTracking, stopCursorTracking, onlineUsers, cursors, isConnected } = useCollaboration(appId, store)
|
||||
const { startCursorTracking, stopCursorTracking, onlineUsers, cursors, isConnected } = useCollaboration(appId || '', store)
|
||||
const [myUserId, setMyUserId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -297,10 +296,12 @@ const WorkflowMain = ({
|
|||
viewport={viewport}
|
||||
onWorkflowDataUpdate={handleWorkflowDataUpdate}
|
||||
hooksStore={hooksStore as any}
|
||||
cursors={filteredCursors}
|
||||
myUserId={myUserId}
|
||||
onlineUsers={onlineUsers}
|
||||
>
|
||||
<WorkflowChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
<UserCursors cursors={filteredCursors} myUserId={myUserId} onlineUsers={onlineUsers} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
import produce from 'immer'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { useEventListener } from 'ahooks'
|
||||
|
|
@ -19,9 +18,9 @@ import CustomNode from './nodes'
|
|||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import { BlockEnum } from './types'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const CandidateNode = () => {
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const candidateNode = useStore(s => s.candidateNode)
|
||||
|
|
@ -29,18 +28,15 @@ const CandidateNode = () => {
|
|||
const { zoom } = useViewport()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
const { candidateNode, mousePosition } = workflowStore.getState()
|
||||
|
||||
if (candidateNode) {
|
||||
e.preventDefault()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const nodes = getNodes()
|
||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.push({
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const UserCursors: FC<UserCursorsProps> = ({
|
|||
return (
|
||||
<div
|
||||
key={userId}
|
||||
className="pointer-events-none absolute z-[10000] transition-all duration-150 ease-out"
|
||||
className="pointer-events-none absolute z-[8] transition-all duration-150 ease-out"
|
||||
style={{
|
||||
left: screenPos.x,
|
||||
top: screenPos.y,
|
||||
|
|
|
|||
|
|
@ -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-1 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
|
||||
<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
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { FC } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { MentionInput } from './mention-input'
|
||||
|
|
@ -13,6 +14,7 @@ type CommentInputProps = {
|
|||
|
||||
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel }) => {
|
||||
const [content, setContent] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -46,8 +48,8 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
|
|||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative shrink-0">
|
||||
<div className="relative h-10 w-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-primary-500">
|
||||
<div className="absolute inset-1 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
|
||||
<div className="relative h-8 w-8 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-primary-500">
|
||||
<div className="absolute inset-[2px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="h-6 w-6 overflow-hidden rounded-full">
|
||||
<Avatar
|
||||
|
|
@ -63,15 +65,15 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
|
|||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[9px] shadow-md',
|
||||
'relative z-10 flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[4px] shadow-md',
|
||||
)}
|
||||
>
|
||||
<div className='relative px-[9px] pt-[9px]'>
|
||||
<div className='relative px-[9px] pt-[4px]'>
|
||||
<MentionInput
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
onSubmit={handleMentionSubmit}
|
||||
placeholder="Add a comment"
|
||||
placeholder={t('workflow.comments.placeholder.add')}
|
||||
autoFocus
|
||||
className="relative"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
|||
import { memo } from 'react'
|
||||
import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
import { Comment } from '@/app/components/base/icons/src/public/other'
|
||||
|
||||
type CommentCursorProps = {
|
||||
mousePosition: { elementX: number; elementY: number }
|
||||
|
|
@ -22,10 +23,7 @@ export const CommentCursor: FC<CommentCursorProps> = memo(({ mousePosition }) =>
|
|||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M10.5 6.33325H5.5H10.5ZM8 9.66658H5.5H8ZM0.5 14.6666H11.3333C13.6345 14.6666 15.5 12.8011 15.5 10.4999V5.49992C15.5 3.19874 13.6345 1.33325 11.3333 1.33325H4.66667C2.36548 1.33325 0.5 3.19874 0.5 5.49992V14.6666Z" fill="white"/>
|
||||
<path d="M10.5 6.33325H5.5M8 9.66658H5.5M0.5 14.6666H11.3333C13.6345 14.6666 15.5 12.8011 15.5 10.4999V5.49992C15.5 3.19874 13.6345 1.33325 11.3333 1.33325H4.66667C2.36548 1.33325 0.5 3.19874 0.5 5.49992V14.6666Z" stroke="black"/>
|
||||
</svg>
|
||||
<Comment className="text-text-primary" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowUpLine, RiAtLine } from '@remixicon/react'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
|
@ -29,7 +30,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
placeholder = 'Add a comment',
|
||||
placeholder,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
className,
|
||||
|
|
@ -37,6 +38,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
autoFocus = false,
|
||||
}) => {
|
||||
const params = useParams()
|
||||
const { t } = useTranslation()
|
||||
const appId = params.appId as string
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
|
|
@ -46,6 +48,77 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
const [mentionPosition, setMentionPosition] = useState(0)
|
||||
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0)
|
||||
const [mentionedUserIds, setMentionedUserIds] = useState<string[]>([])
|
||||
const resolvedPlaceholder = placeholder ?? t('workflow.comments.placeholder.add')
|
||||
|
||||
const mentionNameList = useMemo(() => {
|
||||
const names = mentionUsers
|
||||
.map(user => user.name?.trim())
|
||||
.filter((name): name is string => Boolean(name))
|
||||
|
||||
const uniqueNames = Array.from(new Set(names))
|
||||
uniqueNames.sort((a, b) => b.length - a.length)
|
||||
return uniqueNames
|
||||
}, [mentionUsers])
|
||||
|
||||
const highlightedValue = useMemo<ReactNode>(() => {
|
||||
if (!value)
|
||||
return ''
|
||||
|
||||
if (mentionNameList.length === 0)
|
||||
return value
|
||||
|
||||
const segments: ReactNode[] = []
|
||||
let cursor = 0
|
||||
let hasMention = false
|
||||
|
||||
while (cursor < value.length) {
|
||||
let nextMatchStart = -1
|
||||
let matchedName = ''
|
||||
|
||||
for (const name of mentionNameList) {
|
||||
const searchStart = value.indexOf(`@${name}`, cursor)
|
||||
if (searchStart === -1)
|
||||
continue
|
||||
|
||||
const previousChar = searchStart > 0 ? value[searchStart - 1] : ''
|
||||
if (searchStart > 0 && !/\s/.test(previousChar))
|
||||
continue
|
||||
|
||||
if (
|
||||
nextMatchStart === -1
|
||||
|| searchStart < nextMatchStart
|
||||
|| (searchStart === nextMatchStart && name.length > matchedName.length)
|
||||
) {
|
||||
nextMatchStart = searchStart
|
||||
matchedName = name
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMatchStart === -1)
|
||||
break
|
||||
|
||||
if (nextMatchStart > cursor)
|
||||
segments.push(<span key={`text-${cursor}`}>{value.slice(cursor, nextMatchStart)}</span>)
|
||||
|
||||
const mentionEnd = nextMatchStart + matchedName.length + 1
|
||||
segments.push(
|
||||
<span key={`mention-${nextMatchStart}`} className='text-primary-600'>
|
||||
{value.slice(nextMatchStart, mentionEnd)}
|
||||
</span>,
|
||||
)
|
||||
|
||||
hasMention = true
|
||||
cursor = mentionEnd
|
||||
}
|
||||
|
||||
if (!hasMention)
|
||||
return value
|
||||
|
||||
if (cursor < value.length)
|
||||
segments.push(<span key={`text-${cursor}`}>{value.slice(cursor)}</span>)
|
||||
|
||||
return segments
|
||||
}, [value, mentionNameList])
|
||||
|
||||
const loadMentionableUsers = useCallback(async () => {
|
||||
if (!appId) return
|
||||
|
|
@ -220,12 +293,23 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
return (
|
||||
<>
|
||||
<div className={cn('relative flex items-center', className)}>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 z-0 overflow-hidden whitespace-pre-wrap break-words p-1 leading-6',
|
||||
'body-lg-regular text-text-primary',
|
||||
)}
|
||||
>
|
||||
{highlightedValue}
|
||||
{''}
|
||||
</div>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary caret-primary-500 outline-none',
|
||||
'body-lg-regular relative z-10 w-full resize-none bg-transparent p-1 leading-6 text-transparent caret-primary-500 outline-none',
|
||||
'placeholder:text-text-tertiary',
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
placeholder={resolvedPlaceholder}
|
||||
autoFocus={autoFocus}
|
||||
minRows={isEditing ? 4 : 1}
|
||||
maxRows={4}
|
||||
|
|
@ -238,10 +322,10 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
{!isEditing && (
|
||||
<div className="absolute bottom-0 right-1 z-20 flex items-end gap-1">
|
||||
<div
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg hover:bg-state-base-hover"
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={handleMentionButtonClick}
|
||||
>
|
||||
<RiAtLine className="h-4 w-4" />
|
||||
<RiAtLine className="h-4 w-4 text-components-button-primary-text" />
|
||||
</div>
|
||||
<Button
|
||||
className='z-20 ml-2 w-8 px-0'
|
||||
|
|
@ -249,7 +333,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
disabled={!value.trim() || disabled || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<RiArrowUpLine className='h-4 w-4' />
|
||||
<RiArrowUpLine className='h-4 w-4 text-components-button-primary-text' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -257,14 +341,14 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
{isEditing && (
|
||||
<div className="absolute bottom-0 left-1 right-1 z-20 flex items-end justify-between">
|
||||
<div
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg hover:bg-state-base-hover"
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={handleMentionButtonClick}
|
||||
>
|
||||
<RiAtLine className="h-4 w-4" />
|
||||
<RiAtLine className="h-4 w-4 text-components-button-primary-text" />
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button variant='secondary' size='small' onClick={onCancel} disabled={loading}>
|
||||
Cancel
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
|
|
@ -272,7 +356,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
disabled={loading || !value.trim()}
|
||||
onClick={() => handleSubmit()}
|
||||
>
|
||||
Save
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -281,7 +365,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
|||
|
||||
{showMentionDropdown && filteredMentionUsers.length > 0 && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
className="fixed z-[9999] max-h-40 w-64 overflow-y-auto rounded-lg border border-components-panel-border bg-white shadow-lg"
|
||||
className="fixed z-[9999] max-h-40 w-64 overflow-y-auto rounded-lg border border-components-panel-border bg-components-panel-bg shadow-lg"
|
||||
style={{
|
||||
left: dropdownPosition.x,
|
||||
top: dropdownPosition.y,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
|
@ -11,6 +12,7 @@ import { useFormatTimeFromNow } from '@/app/components/workflow/hooks'
|
|||
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { MentionInput } from './mention-input'
|
||||
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
||||
|
||||
type CommentThreadProps = {
|
||||
comment: WorkflowCommentDetail
|
||||
|
|
@ -28,12 +30,81 @@ type CommentThreadProps = {
|
|||
}
|
||||
|
||||
const ThreadMessage: FC<{
|
||||
authorId: string
|
||||
authorName: string
|
||||
avatarUrl?: string | null
|
||||
createdAt: number
|
||||
content: string
|
||||
}> = ({ authorName, avatarUrl, createdAt, content }) => {
|
||||
mentionedNames?: string[]
|
||||
}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionedNames }) => {
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { userProfile } = useAppContext()
|
||||
const currentUserId = userProfile?.id
|
||||
const isCurrentUser = authorId === currentUserId
|
||||
const userColor = isCurrentUser ? undefined : getUserColor(authorId)
|
||||
|
||||
const highlightedContent = useMemo<ReactNode>(() => {
|
||||
if (!content)
|
||||
return ''
|
||||
|
||||
const normalizedNames = Array.from(new Set((mentionedNames || [])
|
||||
.map(name => name.trim())
|
||||
.filter(Boolean)))
|
||||
|
||||
if (normalizedNames.length === 0)
|
||||
return content
|
||||
|
||||
const segments: ReactNode[] = []
|
||||
let hasMention = false
|
||||
let cursor = 0
|
||||
|
||||
while (cursor < content.length) {
|
||||
let nextMatchStart = -1
|
||||
let matchedName = ''
|
||||
|
||||
for (const name of normalizedNames) {
|
||||
const searchStart = content.indexOf(`@${name}`, cursor)
|
||||
if (searchStart === -1)
|
||||
continue
|
||||
|
||||
const previousChar = searchStart > 0 ? content[searchStart - 1] : ''
|
||||
if (searchStart > 0 && !/\s/.test(previousChar))
|
||||
continue
|
||||
|
||||
if (
|
||||
nextMatchStart === -1
|
||||
|| searchStart < nextMatchStart
|
||||
|| (searchStart === nextMatchStart && name.length > matchedName.length)
|
||||
) {
|
||||
nextMatchStart = searchStart
|
||||
matchedName = name
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMatchStart === -1)
|
||||
break
|
||||
|
||||
if (nextMatchStart > cursor)
|
||||
segments.push(<span key={`text-${cursor}`}>{content.slice(cursor, nextMatchStart)}</span>)
|
||||
|
||||
const mentionEnd = nextMatchStart + matchedName.length + 1
|
||||
segments.push(
|
||||
<span key={`mention-${nextMatchStart}`} className='text-primary-600'>
|
||||
{content.slice(nextMatchStart, mentionEnd)}
|
||||
</span>,
|
||||
)
|
||||
hasMention = true
|
||||
cursor = mentionEnd
|
||||
}
|
||||
|
||||
if (!hasMention)
|
||||
return content
|
||||
|
||||
if (cursor < content.length)
|
||||
segments.push(<span key={`text-${cursor}`}>{content.slice(cursor)}</span>)
|
||||
|
||||
return segments
|
||||
}, [content, mentionedNames])
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-3 pt-1')}>
|
||||
|
|
@ -43,6 +114,7 @@ const ThreadMessage: FC<{
|
|||
avatar={avatarUrl || null}
|
||||
size={24}
|
||||
className={cn('h-8 w-8 rounded-full')}
|
||||
backgroundColor={userColor}
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 pb-4 text-text-primary last:pb-0'>
|
||||
|
|
@ -51,7 +123,7 @@ const ThreadMessage: FC<{
|
|||
<span className='system-2xs-regular text-text-tertiary'>{formatTimeFromNow(createdAt * 1000)}</span>
|
||||
</div>
|
||||
<div className='system-sm-regular mt-1 whitespace-pre-wrap break-words text-text-secondary'>
|
||||
{content}
|
||||
{highlightedContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -75,6 +147,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
const { flowToScreenPosition } = useReactFlow()
|
||||
const viewport = useViewport()
|
||||
const { userProfile } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [activeReplyMenuId, setActiveReplyMenuId] = useState<string | null>(null)
|
||||
const [editingReply, setEditingReply] = useState<{ id: string; content: string }>({ id: '', content: '' })
|
||||
|
|
@ -120,26 +193,44 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
}, [editingReply, onReplyEdit])
|
||||
|
||||
const replies = comment.replies || []
|
||||
const mentionsByTarget = useMemo(() => {
|
||||
const map = new Map<string, string[]>()
|
||||
for (const mention of comment.mentions || []) {
|
||||
const name = mention.mentioned_user_account?.name?.trim()
|
||||
if (!name)
|
||||
continue
|
||||
const key = mention.reply_id ?? 'root'
|
||||
const existing = map.get(key)
|
||||
if (existing) {
|
||||
if (!existing.includes(name))
|
||||
existing.push(name)
|
||||
}
|
||||
else {
|
||||
map.set(key, [name])
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [comment.mentions])
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<div className='flex items-center justify-between rounded-t-2xl border-b border-components-panel-border bg-components-panel-bg-blur px-4 py-3'>
|
||||
<div className=' font-semibold uppercase text-text-primary'>Comment</div>
|
||||
<div className='font-semibold uppercase text-text-primary'>{t('workflow.comments.panelTitle')}</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<button
|
||||
type='button'
|
||||
disabled={loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onDelete}
|
||||
aria-label='Delete comment'
|
||||
aria-label={t('workflow.comments.aria.deleteComment')}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</button>
|
||||
|
|
@ -148,7 +239,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
disabled={comment.resolved || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onResolve}
|
||||
aria-label='Resolve comment'
|
||||
aria-label={t('workflow.comments.aria.resolveComment')}
|
||||
>
|
||||
{comment.resolved ? <RiCheckboxCircleFill className='h-4 w-4' /> : <RiCheckboxCircleLine className='h-4 w-4' />}
|
||||
</button>
|
||||
|
|
@ -158,7 +249,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
disabled={!canGoPrev || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onPrev}
|
||||
aria-label='Previous comment'
|
||||
aria-label={t('workflow.comments.aria.previousComment')}
|
||||
>
|
||||
<RiArrowUpSLine className='h-4 w-4' />
|
||||
</button>
|
||||
|
|
@ -167,7 +258,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
disabled={!canGoNext || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onNext}
|
||||
aria-label='Next comment'
|
||||
aria-label={t('workflow.comments.aria.nextComment')}
|
||||
>
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
</button>
|
||||
|
|
@ -175,7 +266,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
type='button'
|
||||
className='flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={onClose}
|
||||
aria-label='Close comment'
|
||||
aria-label={t('workflow.comments.aria.closeComment')}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</button>
|
||||
|
|
@ -183,10 +274,12 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
</div>
|
||||
<div className='relative mt-2 flex-1 overflow-y-auto px-4'>
|
||||
<ThreadMessage
|
||||
authorName={comment.created_by_account?.name || 'User'}
|
||||
authorId={comment.created_by_account?.id || ''}
|
||||
authorName={comment.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
avatarUrl={comment.created_by_account?.avatar_url || null}
|
||||
createdAt={comment.created_at}
|
||||
content={comment.content}
|
||||
mentionedNames={mentionsByTarget.get('root')}
|
||||
/>
|
||||
{replies.length > 0 && (
|
||||
<div className='mt-2 space-y-3 pt-3'>
|
||||
|
|
@ -206,7 +299,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
e.stopPropagation()
|
||||
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
|
||||
}}
|
||||
aria-label='Reply actions'
|
||||
aria-label={t('workflow.comments.aria.replyActions')}
|
||||
>
|
||||
<RiMoreFill className='h-4 w-4' />
|
||||
</button>
|
||||
|
|
@ -216,16 +309,16 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
className='flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => handleStartEdit(reply)}
|
||||
>
|
||||
Edit reply
|
||||
{t('workflow.comments.actions.editReply')}
|
||||
</button>
|
||||
<button
|
||||
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm hover:bg-state-base-hover'
|
||||
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
setActiveReplyMenuId(null)
|
||||
onReplyDelete?.(reply.id)
|
||||
}}
|
||||
>
|
||||
Delete reply
|
||||
{t('workflow.comments.actions.deleteReply')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -238,7 +331,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={handleCancelEdit}
|
||||
placeholder="Edit reply"
|
||||
placeholder={t('workflow.comments.placeholder.editReply')}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
isEditing={true}
|
||||
|
|
@ -248,10 +341,12 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
</div>
|
||||
) : (
|
||||
<ThreadMessage
|
||||
authorName={reply.created_by_account?.name || 'User'}
|
||||
authorId={reply.created_by_account?.id || ''}
|
||||
authorName={reply.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
avatarUrl={reply.created_by_account?.avatar_url || null}
|
||||
createdAt={reply.created_at}
|
||||
content={reply.content}
|
||||
mentionedNames={mentionsByTarget.get(reply.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -262,7 +357,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
</div>
|
||||
{loading && (
|
||||
<div className='bg-components-panel-bg/70 absolute inset-0 z-30 flex items-center justify-center text-sm text-text-tertiary'>
|
||||
Loading…
|
||||
{t('workflow.comments.loading')}
|
||||
</div>
|
||||
)}
|
||||
{onReply && (
|
||||
|
|
@ -270,7 +365,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
<div className='flex items-center gap-3'>
|
||||
<Avatar
|
||||
avatar={userProfile?.avatar_url || null}
|
||||
name={userProfile?.name || 'You'}
|
||||
name={userProfile?.name || t('common.you')}
|
||||
size={24}
|
||||
className='h-8 w-8'
|
||||
/>
|
||||
|
|
@ -279,7 +374,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
|||
value={replyContent}
|
||||
onChange={setReplyContent}
|
||||
onSubmit={handleReplySubmit}
|
||||
placeholder='Reply'
|
||||
placeholder={t('workflow.comments.placeholder.reply')}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
className='px-2'
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import RestoringTitle from './restoring-title'
|
|||
import Button from '@/app/components/base/button'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { useHooksStore } from '../hooks-store'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
|
|
@ -32,6 +33,7 @@ const HeaderInRestoring = ({
|
|||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const OnlineUsers = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center rounded-full bg-white px-1 py-1">
|
||||
<div className="flex items-center rounded-full border border-components-panel-border bg-components-panel-bg px-1 py-1">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center -space-x-2">
|
||||
{visibleUsers.map((user, index) => {
|
||||
|
|
@ -114,7 +114,7 @@ const OnlineUsers = () => {
|
|||
name={user.username || 'User'}
|
||||
avatar={getAvatarUrl(user)}
|
||||
size={28}
|
||||
className="ring-2 ring-white"
|
||||
className="ring-2 ring-components-panel-bg"
|
||||
backgroundColor={userColor}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -138,7 +138,7 @@ const OnlineUsers = () => {
|
|||
'flex h-7 w-7 items-center justify-center',
|
||||
'cursor-pointer rounded-full bg-gray-300',
|
||||
'text-xs font-medium text-gray-700',
|
||||
'ring-2 ring-white',
|
||||
'ring-2 ring-components-panel-bg',
|
||||
)}
|
||||
>
|
||||
+{remainingCount}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,9 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
|||
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
let { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
// temp fix nodeMetaData is undefined
|
||||
const nodeMetaData = nodesExtraData?.[node.data.type]
|
||||
let { errorMessage } = nodeMetaData?.checkValid ? nodeMetaData.checkValid(checkData, t, moreDataForCheckValid) : { errorMessage: undefined }
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const useShortcuts = (): void => {
|
|||
const {
|
||||
handleModeHand,
|
||||
handleModePointer,
|
||||
handleModeComment,
|
||||
} = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize()
|
||||
|
|
@ -142,6 +143,16 @@ export const useShortcuts = (): void => {
|
|||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('c', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleModeComment()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -113,19 +113,16 @@ export const useWorkflowComment = () => {
|
|||
|
||||
await loadComments()
|
||||
setPendingComment(null)
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to create comment:', error)
|
||||
setPendingComment(null)
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}
|
||||
}, [appId, pendingComment, setControlMode, setPendingComment, loadComments, reactflow])
|
||||
}, [appId, pendingComment, setPendingComment, loadComments, reactflow])
|
||||
|
||||
const handleCommentCancel = useCallback(() => {
|
||||
setPendingComment(null)
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}, [setControlMode, setPendingComment])
|
||||
}, [setPendingComment])
|
||||
|
||||
const handleCommentIconClick = useCallback(async (comment: WorkflowCommentList) => {
|
||||
setPendingComment(null)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import {
|
||||
|
|
@ -28,6 +28,7 @@ import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-withou
|
|||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
export const useWorkflowInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
|
@ -70,31 +71,39 @@ export const useWorkflowMoveMode = () => {
|
|||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
const handleModeComment = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setControlMode(ControlMode.Comment)
|
||||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
return {
|
||||
handleModePointer,
|
||||
handleModeHand,
|
||||
handleModeComment,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowOrganize = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const handleLayout = useCallback(async () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
} = collaborativeWorkflow.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
|
||||
const loopAndIterationNodes = nodes.filter(
|
||||
node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
|
||||
|
|
@ -239,7 +248,7 @@ export const useWorkflowOrganize = () => {
|
|||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
}, [getNodesReadOnly, collaborativeWorkflow, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
return {
|
||||
handleLayout,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'
|
|||
import {
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Connection,
|
||||
|
|
@ -35,6 +34,7 @@ import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
|||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useAvailableBlocks } from './use-available-blocks'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
fetchAllCustomTools,
|
||||
|
|
@ -55,26 +55,19 @@ export const useIsChatMode = () => {
|
|||
|
||||
export const useWorkflow = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getAvailableBlocks } = useAvailableBlocks()
|
||||
const { nodesMap } = useNodesMetaData()
|
||||
|
||||
const getNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
return currentNode
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getTreeLeafNodes = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
let startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
|
|
@ -117,14 +110,11 @@ export const useWorkflow = () => {
|
|||
return uniqBy(list, 'id').filter((item: Node) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}, [store, nodesMap])
|
||||
}, [collaborativeWorkflow, nodesMap])
|
||||
|
||||
const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = newNodes || getNodes()
|
||||
const { nodes: oldNodes, edges } = collaborativeWorkflow.getState()
|
||||
const nodes = newNodes || oldNodes
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
const list: Node[] = []
|
||||
|
|
@ -167,14 +157,11 @@ export const useWorkflow = () => {
|
|||
}
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getBeforeNodesInSameBranchIncludeParent = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges)
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes } = collaborativeWorkflow.getState()
|
||||
const node = allNodes.find(n => n.id === nodeId)
|
||||
const parentNodeId = node?.parentId
|
||||
const parentNode = allNodes.find(n => n.id === parentNodeId)
|
||||
|
|
@ -182,14 +169,10 @@ export const useWorkflow = () => {
|
|||
nodes.push(parentNode)
|
||||
|
||||
return nodes
|
||||
}, [getBeforeNodesInSameBranch, store])
|
||||
}, [getBeforeNodesInSameBranch, collaborativeWorkflow])
|
||||
|
||||
const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
if (!currentNode)
|
||||
|
|
@ -213,40 +196,29 @@ export const useWorkflow = () => {
|
|||
})
|
||||
|
||||
return uniqBy(list, 'id')
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getBeforeNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
return getIncomers(node, nodes, edges)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getIterationNodeChildren = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getLoopNodeChildren = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const affectedNodes = findUsedVarNodes(oldValeSelector, allNodes)
|
||||
if (affectedNodes.length > 0) {
|
||||
const newNodes = allNodes.map((node) => {
|
||||
|
|
@ -257,7 +229,7 @@ export const useWorkflow = () => {
|
|||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
|
|
@ -268,11 +240,11 @@ export const useWorkflow = () => {
|
|||
|
||||
const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, varSelector, [])
|
||||
|
||||
|
|
@ -280,7 +252,7 @@ export const useWorkflow = () => {
|
|||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [getAfterNodesInSameBranch, store])
|
||||
}, [getAfterNodesInSameBranch, collaborativeWorkflow])
|
||||
|
||||
const isNodeVarsUsedInNodes = useCallback((node: Node, isChatMode: boolean) => {
|
||||
const outputVars = getNodeOutputVars(node, isChatMode)
|
||||
|
|
@ -291,9 +263,7 @@ export const useWorkflow = () => {
|
|||
}, [isVarUsedInNodes])
|
||||
|
||||
const checkParallelLimit = useCallback((nodeId: string, nodeHandle = 'source') => {
|
||||
const {
|
||||
edges,
|
||||
} = store.getState()
|
||||
const { edges } = collaborativeWorkflow.getState()
|
||||
const connectedEdges = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === nodeHandle)
|
||||
if (connectedEdges.length > MAX_PARALLEL_LIMIT - 1) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
|
|
@ -302,14 +272,10 @@ export const useWorkflow = () => {
|
|||
}
|
||||
|
||||
return true
|
||||
}, [store, workflowStore, t])
|
||||
}, [collaborativeWorkflow, workflowStore, t])
|
||||
|
||||
const getRootNodesById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
const rootNodes: Node[] = []
|
||||
|
|
@ -349,7 +315,7 @@ export const useWorkflow = () => {
|
|||
return uniqBy(rootNodes, 'id')
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getStartNodes = useCallback((nodes: Node[], currentNode?: Node) => {
|
||||
const { id, parentId } = currentNode || {}
|
||||
|
|
@ -402,11 +368,7 @@ export const useWorkflow = () => {
|
|||
}, [t, workflowStore, getStartNodes])
|
||||
|
||||
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
||||
const {
|
||||
edges,
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const sourceNode: Node = nodes.find(node => node.id === source)!
|
||||
const targetNode: Node = nodes.find(node => node.id === target)!
|
||||
|
||||
|
|
@ -445,7 +407,7 @@ export const useWorkflow = () => {
|
|||
}
|
||||
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, checkParallelLimit, getAvailableBlocks])
|
||||
}, [collaborativeWorkflow, checkParallelLimit, getAvailableBlocks])
|
||||
|
||||
return {
|
||||
getNodeById,
|
||||
|
|
@ -550,13 +512,10 @@ export const useNodesReadOnly = () => {
|
|||
}
|
||||
|
||||
export const useIsNodeInIteration = (iterationId: string) => {
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const isNodeInIteration = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!node)
|
||||
|
|
@ -566,20 +525,17 @@ export const useIsNodeInIteration = (iterationId: string) => {
|
|||
return true
|
||||
|
||||
return false
|
||||
}, [iterationId, store])
|
||||
}, [iterationId, collaborativeWorkflow])
|
||||
return {
|
||||
isNodeInIteration,
|
||||
}
|
||||
}
|
||||
|
||||
export const useIsNodeInLoop = (loopId: string) => {
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const isNodeInLoop = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!node)
|
||||
|
|
@ -589,7 +545,7 @@ export const useIsNodeInLoop = (loopId: string) => {
|
|||
return true
|
||||
|
||||
return false
|
||||
}, [loopId, store])
|
||||
}, [loopId, collaborativeWorkflow])
|
||||
return {
|
||||
isNodeInLoop,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
|
@ -9,6 +10,7 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import {
|
||||
useEventListener,
|
||||
|
|
@ -76,6 +78,7 @@ import LimitTips from './limit-tips'
|
|||
import { setupScrollToNodeListener } from './utils/node-navigation'
|
||||
import { CommentCursor, CommentIcon, CommentInput, CommentThread } from './comment'
|
||||
import { useWorkflowComment } from './hooks/use-workflow-comment'
|
||||
import UserCursors from './collaboration/components/user-cursors'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
|
|
@ -119,6 +122,9 @@ export type WorkflowProps = {
|
|||
viewport?: Viewport
|
||||
children?: React.ReactNode
|
||||
onWorkflowDataUpdate?: (v: any) => void
|
||||
cursors?: Record<string, any>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: any[]
|
||||
}
|
||||
export const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodes: originalNodes,
|
||||
|
|
@ -126,13 +132,14 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
viewport,
|
||||
children,
|
||||
onWorkflowDataUpdate,
|
||||
cursors,
|
||||
myUserId,
|
||||
onlineUsers,
|
||||
}) => {
|
||||
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const [isMouseOverCanvas, setIsMouseOverCanvas] = useState(false)
|
||||
const [pendingDeleteCommentId, setPendingDeleteCommentId] = useState<string | null>(null)
|
||||
const [pendingDeleteReply, setPendingDeleteReply] = useState<{ commentId: string; replyId: string } | null>(null)
|
||||
const [nodes, setNodes] = useNodesState(originalNodes)
|
||||
const [edges, setEdges] = useEdgesState(originalEdges)
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
|
|
@ -193,7 +200,10 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
handleCommentReplyUpdate,
|
||||
handleCommentReplyDelete,
|
||||
} = useWorkflowComment()
|
||||
const showUserComments = useStore(s => s.showUserComments)
|
||||
const showUserCursors = useStore(s => s.showUserCursors)
|
||||
const mousePosition = useStore(s => s.mousePosition)
|
||||
const { t } = useTranslation()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === WORKFLOW_DATA_UPDATE) {
|
||||
|
|
@ -234,6 +244,33 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
setTimeout(() => handleRefreshWorkflowDraft(), 500)
|
||||
}, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft])
|
||||
|
||||
// Optimized comment deletion using showConfirm
|
||||
const handleCommentDeleteClick = useCallback((commentId: string) => {
|
||||
if (!showConfirm) {
|
||||
setShowConfirm({
|
||||
title: t('workflow.comments.confirm.deleteThreadTitle'),
|
||||
desc: t('workflow.comments.confirm.deleteThreadDesc'),
|
||||
onConfirm: async () => {
|
||||
await handleCommentDelete(commentId)
|
||||
setShowConfirm(undefined)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [showConfirm, setShowConfirm, handleCommentDelete, t])
|
||||
|
||||
const handleCommentReplyDeleteClick = useCallback((commentId: string, replyId: string) => {
|
||||
if (!showConfirm) {
|
||||
setShowConfirm({
|
||||
title: t('workflow.comments.confirm.deleteReplyTitle'),
|
||||
desc: t('workflow.comments.confirm.deleteReplyDesc'),
|
||||
onConfirm: async () => {
|
||||
await handleCommentReplyDelete(commentId, replyId)
|
||||
setShowConfirm(undefined)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [showConfirm, setShowConfirm, handleCommentReplyDelete, t])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
|
||||
|
||||
|
|
@ -403,30 +440,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
content={showConfirm.desc}
|
||||
/>
|
||||
)}
|
||||
{pendingDeleteCommentId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title='Delete this thread?'
|
||||
content='This action will permanently delete the thread and all its replies. This cannot be undone.'
|
||||
onCancel={() => setPendingDeleteCommentId(null)}
|
||||
onConfirm={async () => {
|
||||
await handleCommentDelete(pendingDeleteCommentId)
|
||||
setPendingDeleteCommentId(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pendingDeleteReply && (
|
||||
<Confirm
|
||||
isShow
|
||||
title='Delete this reply?'
|
||||
content='This reply will be removed permanently.'
|
||||
onCancel={() => setPendingDeleteReply(null)}
|
||||
onConfirm={async () => {
|
||||
await handleCommentReplyDelete(pendingDeleteReply.commentId, pendingDeleteReply.replyId)
|
||||
setPendingDeleteReply(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LimitTips />
|
||||
{controlMode === ControlMode.Comment && isMouseOverCanvas && (
|
||||
<CommentCursor mousePosition={mousePosition} />
|
||||
|
|
@ -445,31 +458,39 @@ 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}
|
||||
/>
|
||||
<Fragment key={comment.id}>
|
||||
<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={() => handleCommentDeleteClick(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 => handleCommentReplyDeleteClick(comment.id, replyId)}
|
||||
canGoPrev={canGoPrev}
|
||||
canGoNext={canGoNext}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
return (showUserComments || controlMode === ControlMode.Comment) ? (
|
||||
<CommentIcon
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
onClick={() => handleCommentIconClick(comment)}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
})}
|
||||
{children}
|
||||
<ReactFlow
|
||||
|
|
@ -523,6 +544,13 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
className="bg-workflow-canvas-workflow-bg"
|
||||
color='var(--color-workflow-canvas-workflow-dot-color)'
|
||||
/>
|
||||
{showUserCursors && cursors && (
|
||||
<UserCursors
|
||||
cursors={cursors}
|
||||
myUserId={myUserId || null}
|
||||
onlineUsers={onlineUsers || []}
|
||||
/>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -530,14 +558,25 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
|
||||
type WorkflowWithInnerContextProps = WorkflowProps & {
|
||||
hooksStore?: Partial<HooksStoreShape>
|
||||
cursors?: Record<string, any>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: any[]
|
||||
}
|
||||
export const WorkflowWithInnerContext = memo(({
|
||||
hooksStore,
|
||||
cursors,
|
||||
myUserId,
|
||||
onlineUsers,
|
||||
...restProps
|
||||
}: WorkflowWithInnerContextProps) => {
|
||||
return (
|
||||
<HooksStoreContextProvider {...hooksStore}>
|
||||
<Workflow {...restProps} />
|
||||
<Workflow
|
||||
{...restProps}
|
||||
cursors={cursors}
|
||||
myUserId={myUserId}
|
||||
onlineUsers={onlineUsers}
|
||||
/>
|
||||
</HooksStoreContextProvider>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
|
|||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
data._children?.length === 1 && (
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const Node: FC<NodeProps<LoopNodeType>> = ({
|
|||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
data._children?.length === 1 && (
|
||||
<AddBlock
|
||||
loopNodeId={id}
|
||||
loopNodeData={data}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import {
|
|||
RiCursorLine,
|
||||
RiFunctionAddLine,
|
||||
RiHand,
|
||||
RiMessage3Line,
|
||||
RiStickyNoteAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { Comment } from '@/app/components/base/icons/src/public/other'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflowCanvasMaximize,
|
||||
|
|
@ -33,9 +33,9 @@ const Control = () => {
|
|||
const { t } = useTranslation()
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const maximizeCanvas = useStore(s => s.maximizeCanvas)
|
||||
const { handleModePointer, handleModeHand } = useWorkflowMoveMode()
|
||||
const { handleModePointer, handleModeHand, handleModeComment } = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
const { handleAddNote, handleAddComment } = useOperator()
|
||||
const { handleAddNote } = useOperator()
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
|
|
@ -50,14 +50,6 @@ const Control = () => {
|
|||
handleAddNote()
|
||||
}
|
||||
|
||||
const addComment = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
e.stopPropagation()
|
||||
handleAddComment()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pointer-events-auto flex flex-col items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'>
|
||||
<AddBlock />
|
||||
|
|
@ -104,9 +96,9 @@ const Control = () => {
|
|||
controlMode === ControlMode.Comment ? 'bg-state-accent-active text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary',
|
||||
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
|
||||
)}
|
||||
onClick={addComment}
|
||||
onClick={handleModeComment}
|
||||
>
|
||||
<RiMessage3Line className='h-4 w-4' />
|
||||
<Comment className='h-4 w-4' />
|
||||
</div>
|
||||
</TipPopup>
|
||||
<Divider className='my-1 w-3.5' />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type { NoteNodeType } from '../note-node/types'
|
|||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { NoteTheme } from '../note-node/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ControlMode } from '../types'
|
||||
|
||||
export const useOperator = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
|
@ -36,14 +35,7 @@ export const useOperator = () => {
|
|||
})
|
||||
}, [workflowStore, userProfile])
|
||||
|
||||
const handleAddComment = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
controlMode: ControlMode.Comment,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleAddNote,
|
||||
handleAddComment,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { memo, useEffect, useMemo, useRef } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { MiniMap } from 'reactflow'
|
||||
import UndoRedo from '../header/undo-redo'
|
||||
import ZoomInOut from './zoom-in-out'
|
||||
import VariableTrigger from '../variable-inspect/trigger'
|
||||
import VariableInspectPanel from '../variable-inspect'
|
||||
import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
|
||||
export type OperatorProps = {
|
||||
handleUndo: () => void
|
||||
|
|
@ -13,6 +14,26 @@ export type OperatorProps = {
|
|||
|
||||
const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
|
||||
const bottomPanelRef = useRef<HTMLDivElement>(null)
|
||||
const [showMiniMap, setShowMiniMap] = useState(true)
|
||||
const showUserCursors = useStore(s => s.showUserCursors)
|
||||
const setShowUserCursors = useStore(s => s.setShowUserCursors)
|
||||
const showUserComments = useStore(s => s.showUserComments)
|
||||
const setShowUserComments = useStore(s => s.setShowUserComments)
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const isCommentMode = controlMode === ControlMode.Comment
|
||||
|
||||
const handleToggleMiniMap = useCallback(() => {
|
||||
setShowMiniMap(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const handleToggleUserCursors = useCallback(() => {
|
||||
setShowUserCursors(!showUserCursors)
|
||||
}, [showUserCursors, setShowUserCursors])
|
||||
|
||||
const handleToggleUserComments = useCallback(() => {
|
||||
setShowUserComments(!showUserComments)
|
||||
}, [showUserComments, setShowUserComments])
|
||||
|
||||
const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth)
|
||||
const rightPanelWidth = useStore(s => s.rightPanelWidth)
|
||||
const setBottomPanelWidth = useStore(s => s.setBottomPanelWidth)
|
||||
|
|
@ -57,18 +78,28 @@ const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
|
|||
</div>
|
||||
<VariableTrigger />
|
||||
<div className='relative'>
|
||||
<MiniMap
|
||||
pannable
|
||||
zoomable
|
||||
style={{
|
||||
width: 102,
|
||||
height: 72,
|
||||
}}
|
||||
maskColor='var(--color-workflow-minimap-bg)'
|
||||
className='!absolute !bottom-10 z-[9] !m-0 !h-[73px] !w-[103px] !rounded-lg !border-[0.5px]
|
||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
|
||||
{showMiniMap && (
|
||||
<MiniMap
|
||||
pannable
|
||||
zoomable
|
||||
style={{
|
||||
width: 102,
|
||||
height: 72,
|
||||
}}
|
||||
maskColor='var(--color-workflow-minimap-bg)'
|
||||
className='!absolute !bottom-10 z-[9] !m-0 !h-[73px] !w-[103px] !rounded-lg !border-[0.5px]
|
||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
|
||||
/>
|
||||
)}
|
||||
<ZoomInOut
|
||||
showMiniMap={showMiniMap}
|
||||
onToggleMiniMap={handleToggleMiniMap}
|
||||
showUserCursors={showUserCursors}
|
||||
onToggleUserCursors={handleToggleUserCursors}
|
||||
showUserComments={showUserComments}
|
||||
onToggleUserComments={handleToggleUserComments}
|
||||
isCommentMode={isCommentMode}
|
||||
/>
|
||||
<ZoomInOut />
|
||||
</div>
|
||||
</div>
|
||||
<VariableInspectPanel />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiFullscreenLine,
|
||||
RiZoomInLine,
|
||||
RiZoomOutLine,
|
||||
} from '@remixicon/react'
|
||||
|
|
@ -38,9 +40,30 @@ enum ZoomType {
|
|||
zoomTo75 = 'zoomTo75',
|
||||
zoomTo100 = 'zoomTo100',
|
||||
zoomTo200 = 'zoomTo200',
|
||||
toggleUserComments = 'toggleUserComments',
|
||||
toggleUserCursors = 'toggleUserCursors',
|
||||
toggleMiniMap = 'toggleMiniMap',
|
||||
}
|
||||
|
||||
const ZoomInOut: FC = () => {
|
||||
type ZoomInOutProps = {
|
||||
showMiniMap?: boolean
|
||||
onToggleMiniMap?: () => void
|
||||
showUserCursors?: boolean
|
||||
onToggleUserCursors?: () => void
|
||||
showUserComments?: boolean
|
||||
onToggleUserComments?: () => void
|
||||
isCommentMode?: boolean
|
||||
}
|
||||
|
||||
const ZoomInOut: FC<ZoomInOutProps> = ({
|
||||
showMiniMap = true,
|
||||
onToggleMiniMap,
|
||||
showUserCursors = true,
|
||||
onToggleUserCursors,
|
||||
showUserComments = true,
|
||||
onToggleUserComments,
|
||||
isCommentMode = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
zoomIn,
|
||||
|
|
@ -78,13 +101,25 @@ const ZoomInOut: FC = () => {
|
|||
key: ZoomType.zoomTo25,
|
||||
text: '25%',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
key: ZoomType.zoomToFit,
|
||||
text: t('workflow.operator.zoomToFit'),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
key: ZoomType.toggleUserComments,
|
||||
text: t('workflow.operator.showUserComments'),
|
||||
},
|
||||
{
|
||||
key: ZoomType.toggleUserCursors,
|
||||
text: t('workflow.operator.showUserCursors'),
|
||||
},
|
||||
{
|
||||
key: ZoomType.toggleMiniMap,
|
||||
text: t('workflow.operator.showMiniMap'),
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
const handleZoom = (type: string) => {
|
||||
|
|
@ -109,6 +144,23 @@ const ZoomInOut: FC = () => {
|
|||
if (type === ZoomType.zoomTo200)
|
||||
zoomTo(2)
|
||||
|
||||
if (type === ZoomType.toggleUserComments) {
|
||||
if (!isCommentMode)
|
||||
onToggleUserComments?.()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ZoomType.toggleUserCursors) {
|
||||
onToggleUserCursors?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ZoomType.toggleMiniMap) {
|
||||
onToggleMiniMap?.()
|
||||
return
|
||||
}
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +230,7 @@ const ZoomInOut: FC = () => {
|
|||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='w-[145px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
|
||||
<div className='w-[192px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
|
||||
{
|
||||
ZOOM_IN_OUT_OPTIONS.map((options, i) => (
|
||||
<Fragment key={i}>
|
||||
|
|
@ -192,10 +244,43 @@ const ZoomInOut: FC = () => {
|
|||
options.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center justify-between space-x-1 rounded-lg py-1.5 pl-3 pr-2 text-text-secondary hover:bg-state-base-hover'
|
||||
className={`system-md-regular flex h-8 cursor-pointer items-center justify-between space-x-1 rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover ${
|
||||
option.key === ZoomType.toggleUserComments && isCommentMode
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleZoom(option.key)}
|
||||
>
|
||||
<span>{option.text}</span>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{option.key === ZoomType.toggleUserComments && showUserComments && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleUserComments && !showUserComments && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleUserCursors && showUserCursors && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleUserCursors && !showUserCursors && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleMiniMap && showMiniMap && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleMiniMap && !showMiniMap && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
{option.key === ZoomType.zoomToFit && (
|
||||
<RiFullscreenLine className='h-4 w-4 text-text-tertiary' />
|
||||
)}
|
||||
{option.key !== ZoomType.toggleUserComments
|
||||
&& option.key !== ZoomType.toggleUserCursors
|
||||
&& option.key !== ZoomType.toggleMiniMap
|
||||
&& option.key !== ZoomType.zoomToFit && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
<span>{option.text}</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-0.5'>
|
||||
{
|
||||
option.key === ZoomType.zoomToFit && (
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ import {
|
|||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { RiBookOpenLine, RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
|
@ -25,15 +22,15 @@ import { useDocLink } from '@/context/i18n'
|
|||
import cn from '@/utils/classnames'
|
||||
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
|
||||
import { updateConversationVariables } from '@/service/workflow'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const ChatVariablePanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const store = useStoreApi()
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
|
||||
const updateChatVarList = useStore(s => s.setConversationVariables)
|
||||
const appId = useStore(s => s.appId)
|
||||
const appId = useStore(s => s.appId) as string
|
||||
const {
|
||||
invalidateConversationVarValues,
|
||||
} = useInspectVarsCrud()
|
||||
|
|
@ -44,27 +41,27 @@ const ChatVariablePanel = () => {
|
|||
|
||||
const [showRemoveVarConfirm, setShowRemoveConfirm] = useState(false)
|
||||
const [cacheForDelete, setCacheForDelete] = useState<ConversationVariable>()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const getEffectedNodes = useCallback((chatVar: ConversationVariable) => {
|
||||
const { getNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes } = collaborativeWorkflow.getState()
|
||||
return findUsedVarNodes(
|
||||
['conversation', chatVar.name],
|
||||
allNodes,
|
||||
)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((chatVar: ConversationVariable) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(chatVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['conversation', chatVar.name], [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [getEffectedNodes, store])
|
||||
}, [getEffectedNodes, collaborativeWorkflow])
|
||||
|
||||
const handleEdit = (chatVar: ConversationVariable) => {
|
||||
setCurrentVar(chatVar)
|
||||
|
|
@ -151,9 +148,9 @@ const ChatVariablePanel = () => {
|
|||
|
||||
// side effects of rename conversation variable
|
||||
if (currentVar.name !== chatVar.name) {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(currentVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['conversation', currentVar.name], ['conversation', chatVar.name])
|
||||
|
||||
|
|
@ -183,7 +180,7 @@ const ChatVariablePanel = () => {
|
|||
// Revert local state on error
|
||||
updateChatVarList(varList)
|
||||
}
|
||||
}, [currentVar, getEffectedNodes, store, updateChatVarList, varList, appId, invalidateConversationVarValues])
|
||||
}, [currentVar, getEffectedNodes, collaborativeWorkflow, updateChatVarList, varList, appId, invalidateConversationVarValues])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ const CommentsPanel = () => {
|
|||
return (
|
||||
<div className={cn('relative flex h-full w-[420px] flex-col rounded-l-2xl border border-components-panel-border bg-components-panel-bg')}>
|
||||
<div className='flex items-center justify-between p-4 pb-2'>
|
||||
<div className='system-xl-semibold text-text-primary'>Comments</div>
|
||||
<div className='system-xl-semibold font-semibold leading-6 text-text-primary'>Comments</div>
|
||||
<div className='relative flex items-center gap-2'>
|
||||
<button
|
||||
className='flex h-8 w-8 items-center justify-center rounded-md bg-white hover:bg-state-base-hover'
|
||||
className='flex h-8 w-8 items-center justify-center rounded-md bg-components-panel-on-panel-item-bg hover:bg-state-base-hover'
|
||||
aria-label='Filter comments'
|
||||
onClick={() => setShowFilter(v => !v)}
|
||||
>
|
||||
|
|
@ -78,21 +78,21 @@ const CommentsPanel = () => {
|
|||
className={cn('flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', filter === 'all' && 'bg-components-panel-on-panel-item-bg')}
|
||||
onClick={() => handleFilterChange('all')}
|
||||
>
|
||||
<span>All</span>
|
||||
<span className='text-text-secondary'>All</span>
|
||||
{filter === 'all' && <RiCheckLine className='h-4 w-4 text-text-secondary' />}
|
||||
</button>
|
||||
<button
|
||||
className={cn('mt-1 flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', filter === 'unresolved' && 'bg-components-panel-on-panel-item-bg')}
|
||||
onClick={() => handleFilterChange('unresolved')}
|
||||
>
|
||||
<span>Unresolved</span>
|
||||
<span className='text-text-secondary'>Unresolved</span>
|
||||
{filter === 'unresolved' && <RiCheckLine className='h-4 w-4 text-text-secondary' />}
|
||||
</button>
|
||||
<button
|
||||
className={cn('mt-1 flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', filter === 'mine' && 'bg-components-panel-on-panel-item-bg')}
|
||||
onClick={() => handleFilterChange('mine')}
|
||||
>
|
||||
<span>Only your threads</span>
|
||||
<span className='text-text-secondary'>Only your threads</span>
|
||||
{filter === 'mine' && <RiCheckLine className='h-4 w-4 text-text-secondary' />}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -130,7 +130,11 @@ const CommentsPanel = () => {
|
|||
) : (
|
||||
<RiCheckboxCircleLine
|
||||
className='h-4 w-4 cursor-pointer text-text-tertiary hover:text-text-secondary'
|
||||
onClick={() => handleResolve(c)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleResolve(c)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -147,11 +151,13 @@ const CommentsPanel = () => {
|
|||
{/* Content */}
|
||||
<div className='system-sm-regular mt-1 line-clamp-3 break-words text-text-secondary'>{c.content}</div>
|
||||
{/* Footer */}
|
||||
<div className='mt-2 flex items-center justify-between'>
|
||||
<div className='system-2xs-regular text-text-tertiary'>
|
||||
{c.reply_count} replies
|
||||
{c.reply_count > 0 && (
|
||||
<div className='mt-2 flex items-center justify-between'>
|
||||
<div className='system-2xs-regular text-text-tertiary'>
|
||||
{c.reply_count} replies
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ import {
|
|||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
|
@ -20,16 +17,17 @@ import cn from '@/utils/classnames'
|
|||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { updateEnvironmentVariables } from '@/service/workflow'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const EnvPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const envList = useStore(s => s.environmentVariables) as EnvironmentVariable[]
|
||||
const envSecrets = useStore(s => s.envSecrets)
|
||||
const updateEnvList = useStore(s => s.setEnvironmentVariables)
|
||||
const setEnvSecrets = useStore(s => s.setEnvSecrets)
|
||||
const appId = useWorkflowStore(s => s.appId)
|
||||
const appId = useWorkflowStore(s => s.appId) as string
|
||||
|
||||
const [showVariableModal, setShowVariableModal] = useState(false)
|
||||
const [currentVar, setCurrentVar] = useState<EnvironmentVariable>()
|
||||
|
|
@ -42,25 +40,24 @@ const EnvPanel = () => {
|
|||
}
|
||||
|
||||
const getEffectedNodes = useCallback((env: EnvironmentVariable) => {
|
||||
const { getNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes } = collaborativeWorkflow.getState()
|
||||
return findUsedVarNodes(
|
||||
['env', env.name],
|
||||
allNodes,
|
||||
)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((env: EnvironmentVariable) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(env)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['env', env.name], [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [getEffectedNodes, store])
|
||||
}, [getEffectedNodes, collaborativeWorkflow])
|
||||
|
||||
const handleEdit = (env: EnvironmentVariable) => {
|
||||
setCurrentVar(env)
|
||||
|
|
@ -185,9 +182,9 @@ const EnvPanel = () => {
|
|||
|
||||
// side effects of rename env
|
||||
if (currentVar.name !== env.name) {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(currentVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['env', currentVar.name], ['env', env.name])
|
||||
|
||||
|
|
@ -218,7 +215,7 @@ const EnvPanel = () => {
|
|||
// Revert local state on error
|
||||
updateEnvList(envList)
|
||||
}
|
||||
}, [currentVar, envList, envSecrets, getEffectedNodes, setEnvSecrets, store, updateEnvList, appId])
|
||||
}, [currentVar, envList, envSecrets, getEffectedNodes, setEnvSecrets, collaborativeWorkflow, updateEnvList, appId])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
|
||||
import { useStore as useReactFlowStore } from 'reactflow'
|
||||
import {
|
||||
RiAlignBottom,
|
||||
RiAlignCenter,
|
||||
|
|
@ -22,6 +22,7 @@ import { WorkflowHistoryEvent, useWorkflowHistory } from './hooks/use-workflow-h
|
|||
import { useStore } from './store'
|
||||
import { useSelectionInteractions } from './hooks/use-selection-interactions'
|
||||
import { useWorkflowStore } from './store'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
enum AlignType {
|
||||
Left = 'left',
|
||||
|
|
@ -42,8 +43,8 @@ const SelectionContextmenu = () => {
|
|||
const selectionMenu = useStore(s => s.selectionMenu)
|
||||
|
||||
// Access React Flow methods
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
// Get selected nodes for alignment logic
|
||||
const selectedNodes = useReactFlowStore(state =>
|
||||
|
|
@ -256,7 +257,7 @@ const SelectionContextmenu = () => {
|
|||
workflowStore.setState({ nodeAnimation: false })
|
||||
|
||||
// Get all current nodes
|
||||
const nodes = store.getState().getNodes()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
|
||||
// Get all selected nodes
|
||||
const selectedNodeIds = selectedNodes.map(node => node.id)
|
||||
|
|
@ -312,7 +313,7 @@ const SelectionContextmenu = () => {
|
|||
const distributeNodes = handleDistributeNodes(nodesToAlign, nodes, alignType)
|
||||
if (distributeNodes) {
|
||||
// Apply node distribution updates
|
||||
store.getState().setNodes(distributeNodes)
|
||||
setNodes(distributeNodes)
|
||||
handleSelectionContextmenuCancel()
|
||||
|
||||
// Clear guide lines
|
||||
|
|
@ -347,7 +348,7 @@ const SelectionContextmenu = () => {
|
|||
// Apply node position updates - consistent with handleNodeDrag and handleNodeDragStop
|
||||
try {
|
||||
// Directly use setNodes to update nodes - consistent with handleNodeDrag
|
||||
store.getState().setNodes(newNodes)
|
||||
setNodes(newNodes)
|
||||
|
||||
// Close popup
|
||||
handleSelectionContextmenuCancel()
|
||||
|
|
@ -366,7 +367,7 @@ const SelectionContextmenu = () => {
|
|||
catch (err) {
|
||||
console.error('Failed to update nodes:', err)
|
||||
}
|
||||
}, [store, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, handleSelectionContextmenuCancel, handleAlignNode, handleDistributeNodes])
|
||||
}, [collaborativeWorkflow, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, handleSelectionContextmenuCancel, handleAlignNode, handleDistributeNodes])
|
||||
|
||||
if (!selectionMenu)
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export type PanelSliceShape = {
|
|||
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
|
||||
showCommentsPanel: boolean
|
||||
setShowCommentsPanel: (showCommentsPanel: boolean) => void
|
||||
showUserComments: boolean
|
||||
setShowUserComments: (showUserComments: boolean) => void
|
||||
showUserCursors: boolean
|
||||
setShowUserCursors: (showUserCursors: boolean) => void
|
||||
panelMenu?: {
|
||||
top: number
|
||||
left: number
|
||||
|
|
@ -42,6 +46,10 @@ export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({
|
|||
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
|
||||
showCommentsPanel: false,
|
||||
setShowCommentsPanel: showCommentsPanel => set(() => ({ showCommentsPanel })),
|
||||
showUserComments: true,
|
||||
setShowUserComments: showUserComments => set(() => ({ showUserComments })),
|
||||
showUserCursors: true,
|
||||
setShowUserCursors: showUserCursors => set(() => ({ showUserCursors })),
|
||||
panelMenu: undefined,
|
||||
setPanelMenu: panelMenu => set(() => ({ panelMenu })),
|
||||
selectionMenu: undefined,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
@import "preflight.css";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
|
||||
@import '../../themes/light.css';
|
||||
@import '../../themes/dark.css';
|
||||
@import "../../themes/manual-light.css";
|
||||
@import "../../themes/manual-dark.css";
|
||||
|
||||
@import "../components/base/button/index.css";
|
||||
@import "../components/base/action-button/index.css";
|
||||
@import "../components/base/modal/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
html {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
|
@ -680,10 +686,6 @@ button:focus-within {
|
|||
display: none;
|
||||
}
|
||||
|
||||
@import "../components/base/button/index.css";
|
||||
@import "../components/base/action-button/index.css";
|
||||
@import "../components/base/modal/index.css";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,36 @@ const translation = {
|
|||
nodeDragStop: 'Knoten verschoben',
|
||||
nodeDelete: 'Knoten gelöscht',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'Kommentar',
|
||||
loading: 'Laden…',
|
||||
placeholder: {
|
||||
add: 'Kommentar hinzufügen',
|
||||
reply: 'Antworten',
|
||||
editReply: 'Antwort bearbeiten',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'Diesen Thread löschen?',
|
||||
deleteThreadDesc: 'Dadurch werden der Thread und alle Antworten dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.',
|
||||
deleteReplyTitle: 'Diese Antwort löschen?',
|
||||
deleteReplyDesc: 'Diese Antwort wird dauerhaft entfernt.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'Kommentar löschen',
|
||||
resolveComment: 'Kommentar abschließen',
|
||||
previousComment: 'Vorheriger Kommentar',
|
||||
nextComment: 'Nächster Kommentar',
|
||||
closeComment: 'Kommentar schließen',
|
||||
replyActions: 'Antwortaktionen',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'Antwort bearbeiten',
|
||||
deleteReply: 'Antwort löschen',
|
||||
},
|
||||
fallback: {
|
||||
user: 'Benutzer',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} ist erforderlich',
|
||||
authRequired: 'Autorisierung ist erforderlich',
|
||||
|
|
|
|||
|
|
@ -198,6 +198,36 @@ const translation = {
|
|||
noteDelete: 'Note deleted',
|
||||
edgeDelete: 'Node disconnected',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'Comment',
|
||||
loading: 'Loading…',
|
||||
placeholder: {
|
||||
add: 'Add a comment',
|
||||
reply: 'Reply',
|
||||
editReply: 'Edit reply',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'Delete this thread?',
|
||||
deleteThreadDesc: 'This action will permanently delete the thread and all its replies. This cannot be undone.',
|
||||
deleteReplyTitle: 'Delete this reply?',
|
||||
deleteReplyDesc: 'This reply will be removed permanently.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'Delete comment',
|
||||
resolveComment: 'Resolve comment',
|
||||
previousComment: 'Previous comment',
|
||||
nextComment: 'Next comment',
|
||||
closeComment: 'Close comment',
|
||||
replyActions: 'Reply actions',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'Edit reply',
|
||||
deleteReply: 'Delete reply',
|
||||
},
|
||||
fallback: {
|
||||
user: 'User',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} is required',
|
||||
rerankModelRequired: 'A configured Rerank Model is required',
|
||||
|
|
@ -302,6 +332,9 @@ const translation = {
|
|||
zoomTo50: 'Zoom to 50%',
|
||||
zoomTo100: 'Zoom to 100%',
|
||||
zoomToFit: 'Zoom to Fit',
|
||||
showUserComments: 'Comments',
|
||||
showUserCursors: 'Collaborator cursors',
|
||||
showMiniMap: 'Minimap',
|
||||
alignNodes: 'Align Nodes',
|
||||
alignLeft: 'Left',
|
||||
alignCenter: 'Center',
|
||||
|
|
|
|||
|
|
@ -192,6 +192,36 @@ const translation = {
|
|||
nodeDescriptionChange: 'Descripción del nodo cambiada',
|
||||
nodeResize: 'Nodo redimensionado',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'Comentario',
|
||||
loading: 'Cargando…',
|
||||
placeholder: {
|
||||
add: 'Añadir un comentario',
|
||||
reply: 'Responder',
|
||||
editReply: 'Editar respuesta',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: '¿Eliminar esta conversación?',
|
||||
deleteThreadDesc: 'Esta acción eliminará permanentemente la conversación y todas sus respuestas. No se puede deshacer.',
|
||||
deleteReplyTitle: '¿Eliminar esta respuesta?',
|
||||
deleteReplyDesc: 'Esta respuesta se eliminará de forma permanente.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'Eliminar comentario',
|
||||
resolveComment: 'Resolver comentario',
|
||||
previousComment: 'Comentario anterior',
|
||||
nextComment: 'Comentario siguiente',
|
||||
closeComment: 'Cerrar comentario',
|
||||
replyActions: 'Acciones de respuesta',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'Editar respuesta',
|
||||
deleteReply: 'Eliminar respuesta',
|
||||
},
|
||||
fallback: {
|
||||
user: 'Usuario',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: 'Se requiere {{field}}',
|
||||
authRequired: 'Se requiere autorización',
|
||||
|
|
|
|||
|
|
@ -192,6 +192,36 @@ const translation = {
|
|||
nodeDescriptionChange: 'شرح نود تغییر کرد',
|
||||
nodeChange: 'نود تغییر کرد',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'دیدگاه',
|
||||
loading: 'در حال بارگذاری…',
|
||||
placeholder: {
|
||||
add: 'افزودن دیدگاه',
|
||||
reply: 'پاسخ',
|
||||
editReply: 'ویرایش پاسخ',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'این گفتگو حذف شود؟',
|
||||
deleteThreadDesc: 'این عملیات گفتگو و تمام پاسخهای آن را برای همیشه حذف میکند و قابل بازگردانی نیست.',
|
||||
deleteReplyTitle: 'این پاسخ حذف شود؟',
|
||||
deleteReplyDesc: 'این پاسخ برای همیشه حذف خواهد شد.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'حذف دیدگاه',
|
||||
resolveComment: 'حلکردن دیدگاه',
|
||||
previousComment: 'دیدگاه قبلی',
|
||||
nextComment: 'دیدگاه بعدی',
|
||||
closeComment: 'بستن دیدگاه',
|
||||
replyActions: 'عملیات پاسخ',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'ویرایش پاسخ',
|
||||
deleteReply: 'حذف پاسخ',
|
||||
},
|
||||
fallback: {
|
||||
user: 'کاربر',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} الزامی است',
|
||||
authRequired: 'احراز هویت ضروری است',
|
||||
|
|
|
|||
|
|
@ -192,6 +192,36 @@ const translation = {
|
|||
nodeAdd: 'Nœud ajouté',
|
||||
nodeDescriptionChange: 'La description du nœud a changé',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'Commentaire',
|
||||
loading: 'Chargement…',
|
||||
placeholder: {
|
||||
add: 'Ajouter un commentaire',
|
||||
reply: 'Répondre',
|
||||
editReply: 'Modifier la réponse',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'Supprimer cette conversation ?',
|
||||
deleteThreadDesc: 'Cette action supprimera définitivement la conversation et toutes ses réponses. Cette opération est irréversible.',
|
||||
deleteReplyTitle: 'Supprimer cette réponse ?',
|
||||
deleteReplyDesc: 'Cette réponse sera supprimée définitivement.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'Supprimer le commentaire',
|
||||
resolveComment: 'Résoudre le commentaire',
|
||||
previousComment: 'Commentaire précédent',
|
||||
nextComment: 'Commentaire suivant',
|
||||
closeComment: 'Fermer le commentaire',
|
||||
replyActions: 'Actions de réponse',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'Modifier la réponse',
|
||||
deleteReply: 'Supprimer la réponse',
|
||||
},
|
||||
fallback: {
|
||||
user: 'Utilisateur',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} est requis',
|
||||
authRequired: 'Autorisation requise',
|
||||
|
|
|
|||
|
|
@ -195,6 +195,36 @@ const translation = {
|
|||
nodePaste: 'नोड चिपका हुआ',
|
||||
nodeDescriptionChange: 'नोड का वर्णन बदल गया',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'टिप्पणी',
|
||||
loading: 'लोड हो रहा है…',
|
||||
placeholder: {
|
||||
add: 'टिप्पणी जोड़ें',
|
||||
reply: 'जवाब दें',
|
||||
editReply: 'जवाब संपादित करें',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'इस थ्रेड को हटाएं?',
|
||||
deleteThreadDesc: 'यह क्रिया थ्रेड और उसकी सभी प्रतिक्रियाओं को स्थायी रूप से हटा देगी। इसे पूर्ववत नहीं किया जा सकता।',
|
||||
deleteReplyTitle: 'इस जवाब को हटाएं?',
|
||||
deleteReplyDesc: 'यह जवाब हमेशा के लिए हटा दिया जाएगा।',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'टिप्पणी हटाएं',
|
||||
resolveComment: 'टिप्पणी समाधान करें',
|
||||
previousComment: 'पिछली टिप्पणी',
|
||||
nextComment: 'अगली टिप्पणी',
|
||||
closeComment: 'टिप्पणी बंद करें',
|
||||
replyActions: 'जवाब क्रियाएं',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'जवाब संपादित करें',
|
||||
deleteReply: 'जवाब हटाएं',
|
||||
},
|
||||
fallback: {
|
||||
user: 'उपयोगकर्ता',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} आवश्यक है',
|
||||
authRequired: 'प्राधिकरण आवश्यक है',
|
||||
|
|
|
|||
|
|
@ -186,6 +186,36 @@ const translation = {
|
|||
edgeDelete: 'Node terputus',
|
||||
nodeChange: 'Node diubah',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'Komentar',
|
||||
loading: 'Memuat…',
|
||||
placeholder: {
|
||||
add: 'Tambahkan komentar',
|
||||
reply: 'Balas',
|
||||
editReply: 'Edit balasan',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'Hapus percakapan ini?',
|
||||
deleteThreadDesc: 'Tindakan ini akan menghapus percakapan dan semua balasannya secara permanen. Tidak dapat dibatalkan.',
|
||||
deleteReplyTitle: 'Hapus balasan ini?',
|
||||
deleteReplyDesc: 'Balasan ini akan dihapus secara permanen.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'Hapus komentar',
|
||||
resolveComment: 'Selesaikan komentar',
|
||||
previousComment: 'Komentar sebelumnya',
|
||||
nextComment: 'Komentar berikutnya',
|
||||
closeComment: 'Tutup komentar',
|
||||
replyActions: 'Aksi balasan',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'Edit balasan',
|
||||
deleteReply: 'Hapus balasan',
|
||||
},
|
||||
fallback: {
|
||||
user: 'Pengguna',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fields: {
|
||||
variable: 'Nama Variabel',
|
||||
|
|
|
|||
|
|
@ -197,6 +197,36 @@ const translation = {
|
|||
nodeDragStop: 'Nodo spostato',
|
||||
nodeConnect: 'Nodo connesso',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'Commento',
|
||||
loading: 'Caricamento…',
|
||||
placeholder: {
|
||||
add: 'Aggiungi un commento',
|
||||
reply: 'Rispondi',
|
||||
editReply: 'Modifica risposta',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'Eliminare questa conversazione?',
|
||||
deleteThreadDesc: 'Questa azione eliminerà in modo permanente la conversazione e tutte le risposte. Non sarà possibile annullare.',
|
||||
deleteReplyTitle: 'Eliminare questa risposta?',
|
||||
deleteReplyDesc: 'Questa risposta verrà rimossa definitivamente.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'Elimina commento',
|
||||
resolveComment: 'Risolvi commento',
|
||||
previousComment: 'Commento precedente',
|
||||
nextComment: 'Commento successivo',
|
||||
closeComment: 'Chiudi commento',
|
||||
replyActions: 'Azioni di risposta',
|
||||
},
|
||||
actions: {
|
||||
editReply: 'Modifica risposta',
|
||||
deleteReply: 'Elimina risposta',
|
||||
},
|
||||
fallback: {
|
||||
user: 'Utente',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} è richiesto',
|
||||
authRequired: 'È richiesta l\'autorizzazione',
|
||||
|
|
|
|||
|
|
@ -197,6 +197,36 @@ const translation = {
|
|||
noteDelete: '注釈が削除されました',
|
||||
edgeDelete: 'ブロックの接続が解除されました',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: 'コメント',
|
||||
loading: '読み込み中…',
|
||||
placeholder: {
|
||||
add: 'コメントを追加',
|
||||
reply: '返信',
|
||||
editReply: '返信を編集',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: 'このスレッドを削除しますか?',
|
||||
deleteThreadDesc: 'この操作によりスレッドとその返信がすべて完全に削除され、元に戻せません。',
|
||||
deleteReplyTitle: 'この返信を削除しますか?',
|
||||
deleteReplyDesc: 'この返信は完全に削除されます。',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: 'コメントを削除',
|
||||
resolveComment: 'コメントを解決',
|
||||
previousComment: '前のコメント',
|
||||
nextComment: '次のコメント',
|
||||
closeComment: 'コメントを閉じる',
|
||||
replyActions: '返信アクション',
|
||||
},
|
||||
actions: {
|
||||
editReply: '返信を編集',
|
||||
deleteReply: '返信を削除',
|
||||
},
|
||||
fallback: {
|
||||
user: 'ユーザー',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} は必須です',
|
||||
rerankModelRequired: 'Rerank モデルが設定されていません',
|
||||
|
|
|
|||
|
|
@ -200,6 +200,36 @@ const translation = {
|
|||
edgeDelete: '노드가 연결이 끊어졌습니다.',
|
||||
nodeTitleChange: '노드 제목이 변경됨',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: '댓글',
|
||||
loading: '불러오는 중…',
|
||||
placeholder: {
|
||||
add: '댓글 추가',
|
||||
reply: '답글',
|
||||
editReply: '답글 편집',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: '이 스레드를 삭제하시겠습니까?',
|
||||
deleteThreadDesc: '이 작업을 수행하면 스레드와 모든 답글이 영구적으로 삭제되며 되돌릴 수 없습니다.',
|
||||
deleteReplyTitle: '이 답글을 삭제하시겠습니까?',
|
||||
deleteReplyDesc: '이 답글은 영구적으로 삭제됩니다.',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: '댓글 삭제',
|
||||
resolveComment: '댓글 해결',
|
||||
previousComment: '이전 댓글',
|
||||
nextComment: '다음 댓글',
|
||||
closeComment: '댓글 닫기',
|
||||
replyActions: '답글 작업',
|
||||
},
|
||||
actions: {
|
||||
editReply: '답글 편집',
|
||||
deleteReply: '답글 삭제',
|
||||
},
|
||||
fallback: {
|
||||
user: '사용자',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}}가 필요합니다',
|
||||
authRequired: '인증이 필요합니다',
|
||||
|
|
|
|||
|
|
@ -198,6 +198,36 @@ const translation = {
|
|||
noteDelete: '注释已删除',
|
||||
edgeDelete: '块已断开连接',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: '评论',
|
||||
loading: '加载中…',
|
||||
placeholder: {
|
||||
add: '添加评论',
|
||||
reply: '回复',
|
||||
editReply: '编辑回复',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: '删除此讨论?',
|
||||
deleteThreadDesc: '此操作会永久删除该讨论及其所有回复,且无法恢复。',
|
||||
deleteReplyTitle: '删除此回复?',
|
||||
deleteReplyDesc: '此回复将被永久删除。',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: '删除评论',
|
||||
resolveComment: '解决评论',
|
||||
previousComment: '上一条评论',
|
||||
nextComment: '下一条评论',
|
||||
closeComment: '关闭评论',
|
||||
replyActions: '回复操作',
|
||||
},
|
||||
actions: {
|
||||
editReply: '编辑回复',
|
||||
deleteReply: '删除回复',
|
||||
},
|
||||
fallback: {
|
||||
user: '用户',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} 不能为空',
|
||||
rerankModelRequired: '未配置 Rerank 模型',
|
||||
|
|
|
|||
|
|
@ -192,6 +192,36 @@ const translation = {
|
|||
edgeDelete: '區塊已斷開連接',
|
||||
noteDelete: '註釋已刪除',
|
||||
},
|
||||
comments: {
|
||||
panelTitle: '評論',
|
||||
loading: '載入中…',
|
||||
placeholder: {
|
||||
add: '新增評論',
|
||||
reply: '回覆',
|
||||
editReply: '編輯回覆',
|
||||
},
|
||||
confirm: {
|
||||
deleteThreadTitle: '刪除此討論串?',
|
||||
deleteThreadDesc: '此操作會永久刪除該討論串及其所有回覆,且無法復原。',
|
||||
deleteReplyTitle: '刪除此回覆?',
|
||||
deleteReplyDesc: '此回覆將被永久刪除。',
|
||||
},
|
||||
aria: {
|
||||
deleteComment: '刪除評論',
|
||||
resolveComment: '解決評論',
|
||||
previousComment: '上一則評論',
|
||||
nextComment: '下一則評論',
|
||||
closeComment: '關閉評論',
|
||||
replyActions: '回覆操作',
|
||||
},
|
||||
actions: {
|
||||
editReply: '編輯回覆',
|
||||
deleteReply: '刪除回覆',
|
||||
},
|
||||
fallback: {
|
||||
user: '使用者',
|
||||
},
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} 不能為空',
|
||||
authRequired: '請先授權',
|
||||
|
|
|
|||
|
|
@ -159,6 +159,20 @@ export type AppVoicesListResponse = [{
|
|||
value: string
|
||||
}]
|
||||
|
||||
export type WorkflowOnlineUser = {
|
||||
user_id?: string
|
||||
username?: string
|
||||
avatar?: string | null
|
||||
sid?: string
|
||||
}
|
||||
|
||||
export type WorkflowOnlineUsersResponse = {
|
||||
data: Record<string, WorkflowOnlineUser[]> | Array<{
|
||||
workflow_id: string
|
||||
users: WorkflowOnlineUser[]
|
||||
}>
|
||||
}
|
||||
|
||||
export type TracingStatus = {
|
||||
enabled: boolean
|
||||
tracing_provider: TracingProvider | null
|
||||
|
|
|
|||
|
|
@ -91,6 +91,11 @@ const remoteImageURLs = [hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WE
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
|
||||
turbopack: {
|
||||
rules: codeInspectorPlugin({
|
||||
bundler: 'turbopack'
|
||||
})
|
||||
},
|
||||
webpack: (config, { dev, isServer }) => {
|
||||
config.plugins.push(codeInspectorPlugin({ bundler: 'webpack' }))
|
||||
|
||||
|
|
@ -118,6 +123,10 @@ const nextConfig = {
|
|||
})),
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: [
|
||||
'@remixicon/react',
|
||||
'@heroicons/react'
|
||||
],
|
||||
},
|
||||
// fix all before production. Now it slow the develop speed.
|
||||
eslint: {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"and_qq >= 14.9"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS='--inspect' next dev",
|
||||
"dev": "cross-env NODE_OPTIONS='--inspect' next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:docker": "next build && node scripts/optimize-standalone.js",
|
||||
"start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js",
|
||||
|
|
@ -205,7 +205,7 @@
|
|||
"autoprefixer": "^10.4.20",
|
||||
"babel-loader": "^10.0.0",
|
||||
"bing-translate-api": "^4.0.2",
|
||||
"code-inspector-plugin": "^0.18.1",
|
||||
"code-inspector-plugin": "1.2.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-next": "15.5.0",
|
||||
|
|
|
|||
|
|
@ -525,8 +525,8 @@ importers:
|
|||
specifier: ^4.0.2
|
||||
version: 4.1.0
|
||||
code-inspector-plugin:
|
||||
specifier: ^0.18.1
|
||||
version: 0.18.3
|
||||
specifier: 1.2.9
|
||||
version: 1.2.9
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
|
|
@ -1378,6 +1378,24 @@ packages:
|
|||
'@clack/prompts@0.11.0':
|
||||
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
||||
|
||||
'@code-inspector/core@1.2.9':
|
||||
resolution: {integrity: sha512-A1w+G73HlTB6S8X6sA6tT+ziWHTAcTyH+7FZ1Sgd3ZLXF/E/jT+hgRbKposjXMwxcbodRc6hBG6UyiV+VxwE6Q==}
|
||||
|
||||
'@code-inspector/esbuild@1.2.9':
|
||||
resolution: {integrity: sha512-DuyfxGupV43CN8YElIqynAniBtE86i037+3OVJYrm3jlJscXzbV98/kOzvu+VJQQvElcDgpgD6C/aGmPvFEiUg==}
|
||||
|
||||
'@code-inspector/mako@1.2.9':
|
||||
resolution: {integrity: sha512-8N+MHdr64AnthLB4v+YGe8/9bgog3BnkxIW/fqX5iVS0X06mF7X1pxfZOD2bABVtv1tW25lRtNs5AgvYJs0vpg==}
|
||||
|
||||
'@code-inspector/turbopack@1.2.9':
|
||||
resolution: {integrity: sha512-UVOUbqU6rpi5eOkrFamKrdeSWb0/OFFJQBaxbgs1RK5V5f4/iVwC5KjO2wkjv8cOGU4EppLfBVSBI1ysOo8S5A==}
|
||||
|
||||
'@code-inspector/vite@1.2.9':
|
||||
resolution: {integrity: sha512-saIokJ3o3SdrHEgTEg1fbbowbKfh7J4mYtu0i1mVfah1b1UfdCF/iFHTEJ6SADMiY47TeNZTg0TQWTlU1AWPww==}
|
||||
|
||||
'@code-inspector/webpack@1.2.9':
|
||||
resolution: {integrity: sha512-9YEykVrOIc0zMV7pyTyZhCprjScjn6gPPmxb4/OQXKCrP2fAm+NB188rg0s95e4sM7U3qRUpPA4NUH5F7Ogo+g==}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -4478,11 +4496,8 @@ packages:
|
|||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
|
||||
code-inspector-core@0.18.3:
|
||||
resolution: {integrity: sha512-60pT2cPoguMTUYdN1MMpjoPUnuF0ud/u7M2y+Vqit/bniLEit9dySEWAVxLU/Ukc5ILrDeLKEttc6fCMl9RUrA==}
|
||||
|
||||
code-inspector-plugin@0.18.3:
|
||||
resolution: {integrity: sha512-d9oJXZUsnvfTaQDwFmDNA2F+AR/TXIxWg1rr8KGcEskltR2prbZsfuu1z70EAn4khpx0smfi/PvIIwNJQ7FAMw==}
|
||||
code-inspector-plugin@1.2.9:
|
||||
resolution: {integrity: sha512-PGp/AQ03vaajimG9rn5+eQHGifrym5CSNLCViPtwzot7FM3MqEkGNqcvimH0FVuv3wDOcP5KvETAUSLf1BE3HA==}
|
||||
|
||||
collapse-white-space@2.1.0:
|
||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||
|
|
@ -5124,9 +5139,6 @@ packages:
|
|||
esast-util-from-js@2.0.1:
|
||||
resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
|
||||
|
||||
esbuild-code-inspector-plugin@0.18.3:
|
||||
resolution: {integrity: sha512-FaPt5eFMtW1oXMWqAcqfAJByNagP1V/R9dwDDLQO29JmryMF35+frskTqy+G53whmTaVi19+TCrFqhNbMZH5ZQ==}
|
||||
|
||||
esbuild-register@3.6.0:
|
||||
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
|
||||
peerDependencies:
|
||||
|
|
@ -6482,8 +6494,8 @@ packages:
|
|||
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
launch-ide@1.0.1:
|
||||
resolution: {integrity: sha512-U7qBxSNk774PxWq4XbmRe0ThiIstPoa4sMH/OGSYxrFVvg8x3biXcF1fsH6wasDpEmEXMdINUrQhBdwsSgKyMg==}
|
||||
launch-ide@1.2.0:
|
||||
resolution: {integrity: sha512-7nXSPQOt3b2JT52Ge8jp4miFcY+nrUEZxNLWBzrEfjmByDTb9b5ytqMSwGhsNwY6Cntwop+6n7rWIFN0+S8PTw==}
|
||||
|
||||
layout-base@1.0.2:
|
||||
resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
|
||||
|
|
@ -8773,9 +8785,6 @@ packages:
|
|||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite-code-inspector-plugin@0.18.3:
|
||||
resolution: {integrity: sha512-178H73vbDUHE+JpvfAfioUHlUr7qXCYIEa2YNXtzenFQGOjtae59P1jjcxGfa6pPHEnOoaitb13K+0qxwhi/WA==}
|
||||
|
||||
vm-browserify@1.1.2:
|
||||
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
||||
|
||||
|
|
@ -8834,9 +8843,6 @@ packages:
|
|||
engines: {node: '>= 10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
webpack-code-inspector-plugin@0.18.3:
|
||||
resolution: {integrity: sha512-3782rsJhBnRiw0IpR6EqnyGDQoiSq0CcGeLJ52rZXlszYCe8igXtcujq7OhI0byaivWQ1LW7sXKyMEoVpBhq0w==}
|
||||
|
||||
webpack-dev-middleware@6.1.3:
|
||||
resolution: {integrity: sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==}
|
||||
engines: {node: '>= 14.15.0'}
|
||||
|
|
@ -10089,6 +10095,48 @@ snapshots:
|
|||
picocolors: 1.1.1
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@code-inspector/core@1.2.9':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.17
|
||||
chalk: 4.1.2
|
||||
dotenv: 16.6.1
|
||||
launch-ide: 1.2.0
|
||||
portfinder: 1.0.37
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/esbuild@1.2.9':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.2.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/mako@1.2.9':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.2.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/turbopack@1.2.9':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.2.9
|
||||
'@code-inspector/webpack': 1.2.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/vite@1.2.9':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.2.9
|
||||
chalk: 4.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@code-inspector/webpack@1.2.9':
|
||||
dependencies:
|
||||
'@code-inspector/core': 1.2.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
|
@ -10232,8 +10280,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@eslint-react/eff': 1.52.3
|
||||
'@typescript-eslint/types': 8.37.0
|
||||
'@typescript-eslint/typescript-estree': 8.44.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.38.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
string-ts: 2.2.1
|
||||
ts-pattern: 5.7.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -10251,7 +10299,7 @@ snapshots:
|
|||
'@typescript-eslint/scope-manager': 8.37.0
|
||||
'@typescript-eslint/type-utils': 8.37.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
'@typescript-eslint/types': 8.37.0
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.38.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
birecord: 0.1.1
|
||||
ts-pattern: 5.7.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -10312,7 +10360,7 @@ snapshots:
|
|||
'@eslint-react/eff': 1.52.3
|
||||
'@typescript-eslint/scope-manager': 8.37.0
|
||||
'@typescript-eslint/types': 8.37.0
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.38.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
string-ts: 2.2.1
|
||||
ts-pattern: 5.7.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -12607,8 +12655,8 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.38.0
|
||||
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
|
||||
'@typescript-eslint/visitor-keys': 8.38.0
|
||||
debug: 4.4.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
debug: 4.4.0
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -12671,9 +12719,9 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 8.37.0
|
||||
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.37.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
debug: 4.4.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
'@typescript-eslint/utils': 8.37.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
debug: 4.4.0
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -12683,9 +12731,9 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 8.38.0
|
||||
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.38.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
'@typescript-eslint/utils': 8.38.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.8.3)
|
||||
debug: 4.4.0
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -12897,7 +12945,7 @@ snapshots:
|
|||
|
||||
'@vue/compiler-core@3.5.17':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.0
|
||||
'@babel/parser': 7.28.4
|
||||
'@vue/shared': 3.5.17
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
|
|
@ -13601,24 +13649,15 @@ snapshots:
|
|||
|
||||
co@4.6.0: {}
|
||||
|
||||
code-inspector-core@0.18.3:
|
||||
code-inspector-plugin@1.2.9:
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.17
|
||||
'@code-inspector/core': 1.2.9
|
||||
'@code-inspector/esbuild': 1.2.9
|
||||
'@code-inspector/mako': 1.2.9
|
||||
'@code-inspector/turbopack': 1.2.9
|
||||
'@code-inspector/vite': 1.2.9
|
||||
'@code-inspector/webpack': 1.2.9
|
||||
chalk: 4.1.1
|
||||
dotenv: 16.6.1
|
||||
launch-ide: 1.0.1
|
||||
portfinder: 1.0.37
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
code-inspector-plugin@0.18.3:
|
||||
dependencies:
|
||||
chalk: 4.1.1
|
||||
code-inspector-core: 0.18.3
|
||||
dotenv: 16.6.1
|
||||
esbuild-code-inspector-plugin: 0.18.3
|
||||
vite-code-inspector-plugin: 0.18.3
|
||||
webpack-code-inspector-plugin: 0.18.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -14276,12 +14315,6 @@ snapshots:
|
|||
esast-util-from-estree: 2.0.0
|
||||
vfile-message: 4.0.2
|
||||
|
||||
esbuild-code-inspector-plugin@0.18.3:
|
||||
dependencies:
|
||||
code-inspector-core: 0.18.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
esbuild-register@3.6.0(esbuild@0.25.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
|
|
@ -14377,8 +14410,8 @@ snapshots:
|
|||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.35.0(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
debug: 4.4.0
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
get-tsconfig: 4.10.1
|
||||
is-bun-module: 2.0.0
|
||||
stable-hash: 0.0.5
|
||||
|
|
@ -14421,7 +14454,7 @@ snapshots:
|
|||
|
||||
eslint-plugin-es-x@7.8.0(eslint@9.35.0(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.35.0(jiti@1.21.7))
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@1.21.7))
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
eslint-compat-utils: 0.5.1(eslint@9.35.0(jiti@1.21.7))
|
||||
|
|
@ -14514,10 +14547,10 @@ snapshots:
|
|||
|
||||
eslint-plugin-n@17.21.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.35.0(jiti@1.21.7))
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@1.21.7))
|
||||
enhanced-resolve: 5.18.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
eslint-plugin-es-x: 7.8.0(eslint@9.35.0(jiti@1.21.7))
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
eslint-plugin-es-x: 7.8.0(eslint@9.32.0(jiti@1.21.7))
|
||||
get-tsconfig: 4.10.1
|
||||
globals: 15.15.0
|
||||
ignore: 5.3.2
|
||||
|
|
@ -14748,9 +14781,9 @@ snapshots:
|
|||
|
||||
eslint-plugin-toml@0.12.0(eslint@9.35.0(jiti@1.21.7)):
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
eslint-compat-utils: 0.6.5(eslint@9.35.0(jiti@1.21.7))
|
||||
debug: 4.4.0
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
eslint-compat-utils: 0.6.5(eslint@9.32.0(jiti@1.21.7))
|
||||
lodash: 4.17.21
|
||||
toml-eslint-parser: 0.10.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -16136,7 +16169,7 @@ snapshots:
|
|||
dependencies:
|
||||
language-subtag-registry: 0.3.23
|
||||
|
||||
launch-ide@1.0.1:
|
||||
launch-ide@1.2.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
dotenv: 16.6.1
|
||||
|
|
@ -18915,12 +18948,6 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.2
|
||||
|
||||
vite-code-inspector-plugin@0.18.3:
|
||||
dependencies:
|
||||
code-inspector-core: 0.18.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vm-browserify@1.1.2: {}
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
|
@ -18944,8 +18971,8 @@ snapshots:
|
|||
|
||||
vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@1.21.7)):
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
eslint: 9.35.0(jiti@1.21.7)
|
||||
debug: 4.4.0
|
||||
eslint: 9.32.0(jiti@1.21.7)
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
|
|
@ -18991,12 +19018,6 @@ snapshots:
|
|||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
webpack-code-inspector-plugin@0.18.3:
|
||||
dependencies:
|
||||
code-inspector-core: 0.18.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
webpack-dev-middleware@6.1.3(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)):
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Fetcher } from 'swr'
|
||||
import { del, get, patch, post, put } from './base'
|
||||
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
|
||||
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse, WorkflowOnlineUser, WorkflowOnlineUsersResponse } from '@/models/app'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
|
||||
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
|
||||
|
|
@ -9,6 +9,31 @@ export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Reco
|
|||
return get<AppListResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const fetchWorkflowOnlineUsers: Fetcher<Record<string, WorkflowOnlineUser[]>, { workflowIds: string[] }> = async ({ workflowIds }) => {
|
||||
if (!workflowIds.length)
|
||||
return {}
|
||||
|
||||
const params = { workflow_ids: workflowIds.join(',') }
|
||||
const response = await get<WorkflowOnlineUsersResponse>('apps/workflows/online-users', { params })
|
||||
|
||||
if (!response || !response.data)
|
||||
return {}
|
||||
|
||||
if (Array.isArray(response.data)) {
|
||||
return response.data.reduce<Record<string, WorkflowOnlineUser[]>>((acc, item) => {
|
||||
if (item?.workflow_id)
|
||||
acc[item.workflow_id] = item.users || []
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
return Object.entries(response.data).reduce<Record<string, WorkflowOnlineUser[]>>((acc, [workflowId, users]) => {
|
||||
if (workflowId)
|
||||
acc[workflowId] = users || []
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const fetchAppDetail: Fetcher<AppDetailResponse, { url: string; id: string }> = ({ url, id }) => {
|
||||
return get<AppDetailResponse>(`${url}/${id}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
"~@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue