Merge branch 'feat/collaboration' into deploy/dev

This commit is contained in:
hjlarry
2025-10-13 16:43:23 +08:00
82 changed files with 966 additions and 500 deletions

View File

@ -1,10 +1,9 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
} from 'react'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine } from '@remixicon/react'
import { BlockEnum } from '../types'
import type {
OnSelectBlock,
@ -14,10 +13,12 @@ import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
import Tools from './tools'
import { ViewType } from './view-type-select'
import cn from '@/utils/classnames'
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { getMarketplaceUrl } from '@/utils/var'
import PluginList, { type ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE } from './constants'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { PluginType } from '../../plugins/types'
import { useGetLanguage } from '@/context/i18n'
type AllToolsProps = {
className?: string
@ -34,9 +35,26 @@ const DataSources = ({
onSelect,
dataSources,
}: AllToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const pluginRef = useRef<ListRef>(null)
const wrapElemRef = useRef<HTMLDivElement>(null)
const isMatchingKeywords = (text: string, keywords: string) => {
return text.toLowerCase().includes(keywords.toLowerCase())
}
const filteredDatasources = useMemo(() => {
const hasFilter = searchText
if (!hasFilter)
return dataSources.filter(toolWithProvider => toolWithProvider.tools.length > 0)
return dataSources.filter((toolWithProvider) => {
return isMatchingKeywords(toolWithProvider.name, searchText) || toolWithProvider.tools.some((tool) => {
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
})
})
}, [searchText, dataSources, language])
const handleSelect = useCallback((_: any, toolDefaultValue: ToolDefaultValue) => {
let defaultValue: DataSourceDefaultValue = {
plugin_id: toolDefaultValue?.provider_id,
@ -55,8 +73,24 @@ const DataSources = ({
}
onSelect(BlockEnum.DataSource, toolDefaultValue && defaultValue)
}, [onSelect])
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const {
queryPluginsWithDebounced: fetchPlugins,
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
useEffect(() => {
if (!enable_marketplace) return
if (searchText) {
fetchPlugins({
query: searchText,
category: PluginType.datasource,
})
}
}, [searchText, enable_marketplace])
return (
<div className={cn(className)}>
<div
@ -66,24 +100,23 @@ const DataSources = ({
>
<Tools
className={toolContentClassName}
tools={dataSources}
tools={filteredDatasources}
onSelect={handleSelect as OnSelectBlock}
viewType={ViewType.flat}
hasSearchText={!!searchText}
canNotSelectMultiple
/>
{
enable_marketplace && (
<Link
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
href={getMarketplaceUrl('')}
target='_blank'
>
<span>{t('plugin.findMoreInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
</Link>
)
}
{/* Plugins from marketplace */}
{enable_marketplace && (
<PluginList
ref={pluginRef}
wrapElemRef={wrapElemRef}
list={notInstalledPlugins}
tags={[]}
searchText={searchText}
toolContentClassName={toolContentClassName}
/>
)}
</div>
</div>
)

View File

@ -372,6 +372,14 @@ export class CollaborationManager {
return this.eventEmitter.on('appStateUpdate', callback)
}
onAppPublishUpdate(callback: (update: any) => void): () => void {
return this.eventEmitter.on('appPublishUpdate', callback)
}
onAppMetaUpdate(callback: (update: any) => void): () => void {
return this.eventEmitter.on('appMetaUpdate', callback)
}
onMcpServerUpdate(callback: (update: any) => void): () => void {
return this.eventEmitter.on('mcpServerUpdate', callback)
}
@ -540,7 +548,7 @@ export class CollaborationManager {
const oldNodesMap = new Map(oldNodes.map(node => [node.id, node]))
const newNodesMap = new Map(newNodes.map(node => [node.id, node]))
const syncDataAllowList = new Set(['_children'])
const syncDataAllowList = new Set(['_children', '_connectedSourceHandleIds', '_connectedTargetHandleIds', '_targetBranches'])
const shouldSyncDataKey = (key: string) => (syncDataAllowList.has(key) || !key.startsWith('_')) && key !== 'selected'
// Delete removed nodes
@ -812,6 +820,14 @@ export class CollaborationManager {
console.log('Processing app_state_update event:', update)
this.eventEmitter.emit('appStateUpdate', update)
}
else if (update.type === 'app_meta_update') {
console.log('Processing app_meta_update event:', update)
this.eventEmitter.emit('appMetaUpdate', update)
}
else if (update.type === 'app_publish_update') {
console.log('Processing app_publish_update event:', update)
this.eventEmitter.emit('appPublishUpdate', update)
}
else if (update.type === 'mcp_server_update') {
console.log('Processing mcp_server_update event:', update)
this.eventEmitter.emit('mcpServerUpdate', update)

View File

@ -50,7 +50,7 @@ export type GraphSyncData = {
}
export type CollaborationUpdate = {
type: 'mouse_move' | 'vars_and_features_update' | 'sync_request' | 'app_state_update' | 'mcp_server_update' | 'workflow_update' | 'comments_update' | 'node_panel_presence'
type: 'mouse_move' | 'vars_and_features_update' | 'sync_request' | 'app_state_update' | 'app_meta_update' | 'mcp_server_update' | 'workflow_update' | 'comments_update' | 'node_panel_presence' | 'app_publish_update'
userId: string
data: any
timestamp: number

View File

@ -22,6 +22,7 @@ import Avatar from '@/app/components/base/avatar'
import cn from '@/utils/classnames'
import { type UserProfile, fetchMentionableUsers } from '@/service/workflow-comment'
import { useStore, useWorkflowStore } from '../store'
import { EnterKey } from '@/app/components/base/icons/src/public/common'
type MentionInputProps = {
value: string
@ -431,6 +432,10 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
}, [value, mentionedUserIds, onSubmit])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
// Ignore key events during IME composition (e.g., Chinese, Japanese input)
if (e.nativeEvent.isComposing)
return
if (showMentionDropdown) {
if (e.key === 'ArrowDown') {
e.preventDefault()
@ -583,9 +588,13 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
size='small'
disabled={loading || !value.trim()}
onClick={() => handleSubmit()}
className='gap-1'
>
{loading && <RiLoader2Line className='mr-1 h-3.5 w-3.5 animate-spin' />}
{t('common.operation.save')}
<span>{t('common.operation.save')}</span>
{!loading && (
<EnterKey className='h-4 w-4' />
)}
</Button>
</div>
</div>
@ -594,7 +603,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
{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-components-panel-bg shadow-lg"
className="bg-components-panel-bg/95 fixed z-[9999] max-h-[248px] w-[280px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border shadow-lg backdrop-blur-[10px]"
style={{
left: dropdownPosition.x,
[dropdownPosition.placement === 'top' ? 'bottom' : 'top']: dropdownPosition.placement === 'top'
@ -607,7 +616,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
<div
key={user.id}
className={cn(
'flex cursor-pointer items-center gap-2 p-2 hover:bg-state-base-hover',
'flex cursor-pointer items-center gap-2 rounded-md py-1 pl-2 pr-3 hover:bg-state-base-hover',
index === selectedMentionIndex && 'bg-state-base-hover',
)}
onClick={() => insertMention(user)}

View File

@ -2,6 +2,7 @@
import type { FC, ReactNode } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'next/navigation'
import { useReactFlow, useViewport } from 'reactflow'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
@ -16,6 +17,7 @@ import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/servic
import { useAppContext } from '@/context/app-context'
import { MentionInput } from './mention-input'
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
import { useStore } from '../store'
type CommentThreadProps = {
comment: WorkflowCommentDetail
@ -41,9 +43,9 @@ const ThreadMessage: FC<{
avatarUrl?: string | null
createdAt: number
content: string
mentionedNames?: string[]
mentionableNames: string[]
className?: string
}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionedNames, className }) => {
}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionableNames, className }) => {
const { formatTimeFromNow } = useFormatTimeFromNow()
const { userProfile } = useAppContext()
const currentUserId = userProfile?.id
@ -54,9 +56,11 @@ const ThreadMessage: FC<{
if (!content)
return ''
const normalizedNames = Array.from(new Set((mentionedNames || [])
// Extract valid user names from mentionableNames, sorted by length (longest first)
const normalizedNames = Array.from(new Set(mentionableNames
.map(name => name.trim())
.filter(Boolean)))
normalizedNames.sort((a, b) => b.length - a.length)
if (normalizedNames.length === 0)
return content
@ -111,7 +115,7 @@ const ThreadMessage: FC<{
segments.push(<span key={`text-${cursor}`}>{content.slice(cursor)}</span>)
return segments
}, [content, mentionedNames])
}, [content, mentionableNames])
return (
<div className={cn('flex gap-3 pt-1', className)}>
@ -154,6 +158,8 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
onReplyDelete,
onReplyDeleteDirect,
}) => {
const params = useParams()
const appId = params.appId as string
const { flowToScreenPosition } = useReactFlow()
const viewport = useViewport()
const { userProfile } = useAppContext()
@ -162,11 +168,26 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
const [activeReplyMenuId, setActiveReplyMenuId] = useState<string | null>(null)
const [editingReply, setEditingReply] = useState<{ id: string; content: string }>({ id: '', content: '' })
const [deletingReplyId, setDeletingReplyId] = useState<string | null>(null)
const [isSubmittingEdit, setIsSubmittingEdit] = useState(false)
// Focus management refs
const replyInputRef = useRef<HTMLTextAreaElement>(null)
const threadRef = useRef<HTMLDivElement>(null)
// Get mentionable users from store
const mentionUsersFromStore = useStore(state => (
appId ? state.mentionableUsersCache[appId] : undefined
))
const mentionUsers = mentionUsersFromStore ?? []
// Extract all mentionable names for highlighting
const mentionableNames = useMemo(() => {
const names = mentionUsers
.map(user => user.name?.trim())
.filter((name): name is string => Boolean(name))
return Array.from(new Set(names))
}, [mentionUsers])
useEffect(() => {
setReplyContent('')
}, [comment.id])
@ -245,13 +266,23 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
if (!onReplyEdit || !editingReply) return
const trimmed = content.trim()
if (!trimmed) return
await onReplyEdit(editingReply.id, trimmed, mentionedUserIds)
setEditingReply({ id: '', content: '' })
// P1: Restore focus to reply input after saving edit
setTimeout(() => {
replyInputRef.current?.focus()
}, 0)
setIsSubmittingEdit(true)
try {
await onReplyEdit(editingReply.id, trimmed, mentionedUserIds)
setEditingReply({ id: '', content: '' })
// P1: Restore focus to reply input after saving edit
setTimeout(() => {
replyInputRef.current?.focus()
}, 0)
}
catch (error) {
console.error('Failed to edit reply', error)
}
finally {
setIsSubmittingEdit(false)
}
}, [editingReply, onReplyEdit])
const replies = comment.replies || []
@ -296,25 +327,6 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
previousReplyCountRef.current = replies.length
}, [comment.id, replies.length])
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]'
@ -414,14 +426,14 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
ref={messageListRef}
className='relative mt-2 flex-1 overflow-y-auto px-4 pb-4'
>
<div className='rounded-lg py-2 pl-1 transition-colors hover:bg-components-panel-on-panel-item-bg-hover'>
<div className='-mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover'>
<ThreadMessage
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')}
mentionableNames={mentionableNames}
/>
</div>
{replies.length > 0 && (
@ -432,7 +444,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
return (
<div
key={reply.id}
className='group relative rounded-lg py-2 pl-1 transition-colors hover:bg-components-panel-on-panel-item-bg-hover'
className='group relative -mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover'
>
{isOwnReply && !isReplyEditing && (
<PortalToFollowElem
@ -468,7 +480,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
</PortalToFollowElemTrigger>
</div>
<PortalToFollowElemContent
className='z-[100] w-36 rounded-xl border border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]'
className='z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]'
data-reply-menu
>
{/* Menu buttons - hidden when showing delete confirm */}
@ -519,19 +531,31 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
</PortalToFollowElem>
)}
{isReplyEditing ? (
<div className='rounded-lg border border-components-chat-input-border bg-components-panel-bg-blur px-3 py-2 shadow-sm'>
<MentionInput
value={editingReply?.content ?? ''}
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
onSubmit={handleEditSubmit}
onCancel={handleCancelEdit}
placeholder={t('workflow.comments.placeholder.editReply')}
disabled={loading}
loading={replyUpdating}
isEditing={true}
className="system-sm-regular"
autoFocus
/>
<div className='flex gap-3 pt-1'>
<div className='shrink-0'>
<Avatar
name={reply.created_by_account?.name || t('workflow.comments.fallback.user')}
avatar={reply.created_by_account?.avatar_url || null}
size={24}
className='h-8 w-8 rounded-full'
/>
</div>
<div className='min-w-0 flex-1'>
<div className='rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1 shadow-md backdrop-blur-[10px]'>
<MentionInput
value={editingReply?.content ?? ''}
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
onSubmit={handleEditSubmit}
onCancel={handleCancelEdit}
placeholder={t('workflow.comments.placeholder.editReply')}
disabled={loading}
loading={replyUpdating || isSubmittingEdit}
isEditing={true}
className="system-sm-regular"
autoFocus
/>
</div>
</div>
</div>
) : (
<ThreadMessage
@ -540,7 +564,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
avatarUrl={reply.created_by_account?.avatar_url || null}
createdAt={reply.created_at}
content={reply.content}
mentionedNames={mentionsByTarget.get(reply.id)}
mentionableNames={mentionableNames}
/>
)}
</div>

View File

@ -0,0 +1,94 @@
import type { ComponentType } from 'react'
import { BlockEnum } from '../types'
import StartNode from './start/node'
import StartPanel from './start/panel'
import EndNode from './end/node'
import EndPanel from './end/panel'
import AnswerNode from './answer/node'
import AnswerPanel from './answer/panel'
import LLMNode from './llm/node'
import LLMPanel from './llm/panel'
import KnowledgeRetrievalNode from './knowledge-retrieval/node'
import KnowledgeRetrievalPanel from './knowledge-retrieval/panel'
import QuestionClassifierNode from './question-classifier/node'
import QuestionClassifierPanel from './question-classifier/panel'
import IfElseNode from './if-else/node'
import IfElsePanel from './if-else/panel'
import CodeNode from './code/node'
import CodePanel from './code/panel'
import TemplateTransformNode from './template-transform/node'
import TemplateTransformPanel from './template-transform/panel'
import HttpNode from './http/node'
import HttpPanel from './http/panel'
import ToolNode from './tool/node'
import ToolPanel from './tool/panel'
import VariableAssignerNode from './variable-assigner/node'
import VariableAssignerPanel from './variable-assigner/panel'
import AssignerNode from './assigner/node'
import AssignerPanel from './assigner/panel'
import ParameterExtractorNode from './parameter-extractor/node'
import ParameterExtractorPanel from './parameter-extractor/panel'
import IterationNode from './iteration/node'
import IterationPanel from './iteration/panel'
import LoopNode from './loop/node'
import LoopPanel from './loop/panel'
import DocExtractorNode from './document-extractor/node'
import DocExtractorPanel from './document-extractor/panel'
import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
import DataSourceNode from './data-source/node'
import DataSourcePanel from './data-source/panel'
import KnowledgeBaseNode from './knowledge-base/node'
import KnowledgeBasePanel from './knowledge-base/panel'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode,
[BlockEnum.End]: EndNode,
[BlockEnum.Answer]: AnswerNode,
[BlockEnum.LLM]: LLMNode,
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
[BlockEnum.IfElse]: IfElseNode,
[BlockEnum.Code]: CodeNode,
[BlockEnum.TemplateTransform]: TemplateTransformNode,
[BlockEnum.HttpRequest]: HttpNode,
[BlockEnum.Tool]: ToolNode,
[BlockEnum.VariableAssigner]: VariableAssignerNode,
[BlockEnum.Assigner]: AssignerNode,
[BlockEnum.VariableAggregator]: VariableAssignerNode,
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.Loop]: LoopNode,
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
[BlockEnum.DataSource]: DataSourceNode,
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartPanel,
[BlockEnum.End]: EndPanel,
[BlockEnum.Answer]: AnswerPanel,
[BlockEnum.LLM]: LLMPanel,
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
[BlockEnum.IfElse]: IfElsePanel,
[BlockEnum.Code]: CodePanel,
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
[BlockEnum.HttpRequest]: HttpPanel,
[BlockEnum.Tool]: ToolPanel,
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
[BlockEnum.Assigner]: AssignerPanel,
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
[BlockEnum.Iteration]: IterationPanel,
[BlockEnum.Loop]: LoopPanel,
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
[BlockEnum.DataSource]: DataSourcePanel,
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
}

View File

@ -1,101 +1,5 @@
import type { ComponentType } from 'react'
import { BlockEnum } from '../types'
import StartNode from './start/node'
import StartPanel from './start/panel'
import EndNode from './end/node'
import EndPanel from './end/panel'
import AnswerNode from './answer/node'
import AnswerPanel from './answer/panel'
import LLMNode from './llm/node'
import LLMPanel from './llm/panel'
import KnowledgeRetrievalNode from './knowledge-retrieval/node'
import KnowledgeRetrievalPanel from './knowledge-retrieval/panel'
import QuestionClassifierNode from './question-classifier/node'
import QuestionClassifierPanel from './question-classifier/panel'
import IfElseNode from './if-else/node'
import IfElsePanel from './if-else/panel'
import CodeNode from './code/node'
import CodePanel from './code/panel'
import TemplateTransformNode from './template-transform/node'
import TemplateTransformPanel from './template-transform/panel'
import HttpNode from './http/node'
import HttpPanel from './http/panel'
import ToolNode from './tool/node'
import ToolPanel from './tool/panel'
import VariableAssignerNode from './variable-assigner/node'
import VariableAssignerPanel from './variable-assigner/panel'
import AssignerNode from './assigner/node'
import AssignerPanel from './assigner/panel'
import ParameterExtractorNode from './parameter-extractor/node'
import ParameterExtractorPanel from './parameter-extractor/panel'
import IterationNode from './iteration/node'
import IterationPanel from './iteration/panel'
import LoopNode from './loop/node'
import LoopPanel from './loop/panel'
import DocExtractorNode from './document-extractor/node'
import DocExtractorPanel from './document-extractor/panel'
import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
import DataSourceNode from './data-source/node'
import DataSourcePanel from './data-source/panel'
import KnowledgeBaseNode from './knowledge-base/node'
import KnowledgeBasePanel from './knowledge-base/panel'
import { TransferMethod } from '@/types/app'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode,
[BlockEnum.End]: EndNode,
[BlockEnum.Answer]: AnswerNode,
[BlockEnum.LLM]: LLMNode,
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
[BlockEnum.IfElse]: IfElseNode,
[BlockEnum.Code]: CodeNode,
[BlockEnum.TemplateTransform]: TemplateTransformNode,
[BlockEnum.HttpRequest]: HttpNode,
[BlockEnum.Tool]: ToolNode,
[BlockEnum.VariableAssigner]: VariableAssignerNode,
[BlockEnum.Assigner]: AssignerNode,
[BlockEnum.VariableAggregator]: VariableAssignerNode,
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.Loop]: LoopNode,
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
[BlockEnum.DataSource]: DataSourceNode,
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartPanel,
[BlockEnum.End]: EndPanel,
[BlockEnum.Answer]: AnswerPanel,
[BlockEnum.LLM]: LLMPanel,
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
[BlockEnum.IfElse]: IfElsePanel,
[BlockEnum.Code]: CodePanel,
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
[BlockEnum.HttpRequest]: HttpPanel,
[BlockEnum.Tool]: ToolPanel,
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
[BlockEnum.Assigner]: AssignerPanel,
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
[BlockEnum.Iteration]: IterationPanel,
[BlockEnum.Loop]: LoopPanel,
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
[BlockEnum.DataSource]: DataSourcePanel,
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
}
export const CUSTOM_NODE_TYPE = 'custom'
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },

View File

@ -8,7 +8,7 @@ import { CUSTOM_NODE } from '../constants'
import {
NodeComponentMap,
PanelComponentMap,
} from './constants'
} from './components'
import BaseNode from './_base/node'
import BasePanel from './_base/components/workflow-panel'

View File

@ -29,7 +29,7 @@ const ChunkStructure = ({
<Field
fieldTitleProps={{
title: t('workflow.nodes.knowledgeBase.chunkStructure'),
tooltip: t('workflow.nodes.knowledgeBase.chunkStructure'),
tooltip: t('workflow.nodes.knowledgeBase.chunkStructureTip.message'),
operation: chunkStructure && (
<Selector
options={options}