Merge branch 'p284' into deploy/dev

This commit is contained in:
hjlarry 2025-09-25 16:58:45 +08:00
commit 96c7c86e9d
57 changed files with 1324 additions and 397 deletions

View File

@ -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}

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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'>

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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'

View File

@ -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,

View File

@ -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>
)
}

View File

@ -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({

View File

@ -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,

View File

@ -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
)
})

View File

@ -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"
/>

View File

@ -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)

View File

@ -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>
)
})

View File

@ -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,

View File

@ -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'

View File

@ -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 {

View File

@ -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}

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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,
}

View File

@ -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>
)
})

View File

@ -58,7 +58,7 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
)
}
{
data._children!.length === 1 && (
data._children?.length === 1 && (
<AddBlock
iterationNodeId={id}
iterationNodeData={data}

View File

@ -46,7 +46,7 @@ const Node: FC<NodeProps<LoopNodeType>> = ({
)
}
{
data._children!.length === 1 && (
data._children?.length === 1 && (
<AddBlock
loopNodeId={id}
loopNodeData={data}

View File

@ -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' />

View File

@ -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,
}
}

View File

@ -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 />

View File

@ -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 && (

View File

@ -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

View File

@ -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>
)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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: 'احراز هویت ضروری است',

View File

@ -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',

View File

@ -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: 'प्राधिकरण आवश्यक है',

View File

@ -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',

View File

@ -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',

View File

@ -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 モデルが設定されていません',

View File

@ -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: '인증이 필요합니다',

View File

@ -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 模型',

View File

@ -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: '請先授權',

View File

@ -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

View File

@ -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: {

View File

@ -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",

View File

@ -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

View File

@ -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}`)
}

View File

@ -26,6 +26,9 @@
"paths": {
"@/*": [
"./*"
],
"~@/*": [
"./*"
]
}
},