mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
refactor workflow
This commit is contained in:
@ -62,13 +62,13 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const { notify } = useToastContext()
|
||||
const ref = useRef(null)
|
||||
const isExternal = currentDataset.provider === 'external'
|
||||
const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(currentDataset?.external_retrieval_model.score_threshold ?? 0.5)
|
||||
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(currentDataset?.external_retrieval_model.score_threshold_enabled ?? false)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const [localeCurrentDataset, setLocaleCurrentDataset] = useState({ ...currentDataset })
|
||||
const [topK, setTopK] = useState(localeCurrentDataset?.external_retrieval_model.top_k ?? 2)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(localeCurrentDataset?.external_retrieval_model.score_threshold ?? 0.5)
|
||||
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(localeCurrentDataset?.external_retrieval_model.score_threshold_enabled ?? false)
|
||||
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset.partial_member_list || [])
|
||||
const [memberList, setMemberList] = useState<Member[]>([])
|
||||
|
||||
@ -88,6 +88,14 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
setScoreThreshold(data.score_threshold)
|
||||
if (data.score_threshold_enabled !== undefined)
|
||||
setScoreThresholdEnabled(data.score_threshold_enabled)
|
||||
|
||||
setLocaleCurrentDataset({
|
||||
...localeCurrentDataset,
|
||||
external_retrieval_model: {
|
||||
...localeCurrentDataset?.external_retrieval_model,
|
||||
...data,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
|
||||
@ -29,7 +29,7 @@ const OPTION_MAP = {
|
||||
iframe: {
|
||||
getContent: (url: string, token: string) =>
|
||||
`<iframe
|
||||
src="${url}${basePath}/chatbot/${token}"
|
||||
src="${url}${basePath}/chat/${token}"
|
||||
style="width: 100%; height: 100%; min-height: 700px"
|
||||
frameborder="0"
|
||||
allow="microphone">
|
||||
@ -42,10 +42,10 @@ const OPTION_MAP = {
|
||||
token: '${token}'${isTestEnv
|
||||
? `,
|
||||
isDev: true`
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}${basePath}'`
|
||||
: ''},
|
||||
: ''},
|
||||
systemVariables: {
|
||||
// user_id: 'YOU CAN DEFINE USER ID HERE',
|
||||
},
|
||||
|
||||
@ -80,8 +80,30 @@ export const useEmbeddedChatbot = () => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (appInfo?.site.default_language)
|
||||
changeLanguage(appInfo.site.default_language)
|
||||
const setLanguageFromParams = async () => {
|
||||
// Check URL parameters for language override
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const localeParam = urlParams.get('locale')
|
||||
|
||||
// Check for encoded system variables
|
||||
const systemVariables = await getProcessedSystemVariablesFromUrlParams()
|
||||
const localeFromSysVar = systemVariables.locale
|
||||
|
||||
if (localeParam) {
|
||||
// If locale parameter exists in URL, use it instead of default
|
||||
changeLanguage(localeParam)
|
||||
}
|
||||
else if (localeFromSysVar) {
|
||||
// If locale is set as a system variable, use that
|
||||
changeLanguage(localeFromSysVar)
|
||||
}
|
||||
else if (appInfo?.site.default_language) {
|
||||
// Otherwise use the default from app config
|
||||
changeLanguage(appInfo.site.default_language)
|
||||
}
|
||||
}
|
||||
|
||||
setLanguageFromParams()
|
||||
}, [appInfo])
|
||||
|
||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
||||
|
||||
@ -1,116 +1,528 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
import { usePrevious } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||
import { cleanUpSvgCode } from './utils'
|
||||
import { MoonIcon, SunIcon } from '@heroicons/react/24/solid'
|
||||
import {
|
||||
cleanUpSvgCode,
|
||||
isMermaidCodeComplete,
|
||||
prepareMermaidCode,
|
||||
processSvgForTheme,
|
||||
svgToBase64,
|
||||
waitForDOMElement,
|
||||
} from './utils'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import cn from '@/utils/classnames'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
let mermaidAPI: any
|
||||
mermaidAPI = null
|
||||
// Global flags and cache for mermaid
|
||||
let isMermaidInitialized = false
|
||||
const diagramCache = new Map<string, string>()
|
||||
let mermaidAPI: any = null
|
||||
|
||||
if (typeof window !== 'undefined')
|
||||
mermaidAPI = mermaid.mermaidAPI
|
||||
|
||||
const svgToBase64 = (svgGraph: string) => {
|
||||
const svgBytes = new TextEncoder().encode(svgGraph)
|
||||
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
// Theme configurations
|
||||
const THEMES = {
|
||||
light: {
|
||||
name: 'Light Theme',
|
||||
background: '#ffffff',
|
||||
primaryColor: '#ffffff',
|
||||
primaryBorderColor: '#000000',
|
||||
primaryTextColor: '#000000',
|
||||
secondaryColor: '#ffffff',
|
||||
tertiaryColor: '#ffffff',
|
||||
nodeColors: [
|
||||
{ bg: '#f0f9ff', color: '#0369a1' },
|
||||
{ bg: '#f0fdf4', color: '#166534' },
|
||||
{ bg: '#fef2f2', color: '#b91c1c' },
|
||||
{ bg: '#faf5ff', color: '#7e22ce' },
|
||||
{ bg: '#fffbeb', color: '#b45309' },
|
||||
],
|
||||
connectionColor: '#74a0e0',
|
||||
},
|
||||
dark: {
|
||||
name: 'Dark Theme',
|
||||
background: '#1e293b',
|
||||
primaryColor: '#334155',
|
||||
primaryBorderColor: '#94a3b8',
|
||||
primaryTextColor: '#e2e8f0',
|
||||
secondaryColor: '#475569',
|
||||
tertiaryColor: '#334155',
|
||||
nodeColors: [
|
||||
{ bg: '#164e63', color: '#e0f2fe' },
|
||||
{ bg: '#14532d', color: '#dcfce7' },
|
||||
{ bg: '#7f1d1d', color: '#fee2e2' },
|
||||
{ bg: '#581c87', color: '#f3e8ff' },
|
||||
{ bg: '#78350f', color: '#fef3c7' },
|
||||
],
|
||||
connectionColor: '#60a5fa',
|
||||
},
|
||||
}
|
||||
|
||||
const Flowchart = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: {
|
||||
PrimitiveCode: string
|
||||
} & {
|
||||
ref: React.RefObject<unknown>;
|
||||
},
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const [svgCode, setSvgCode] = useState(null)
|
||||
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
|
||||
|
||||
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const timeRef = useRef<number>(0)
|
||||
const [errMsg, setErrMsg] = useState('')
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
const renderFlowchart = useCallback(async (PrimitiveCode: string) => {
|
||||
setSvgCode(null)
|
||||
setIsLoading(true)
|
||||
|
||||
/**
|
||||
* Initializes mermaid library with default configuration
|
||||
*/
|
||||
const initMermaid = () => {
|
||||
if (typeof window !== 'undefined' && !isMermaidInitialized) {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && mermaidAPI) {
|
||||
const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
|
||||
const base64Svg: any = await svgToBase64(cleanUpSvgCode(svgGraph.svg))
|
||||
setSvgCode(base64Svg)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (prevPrimitiveCode === props.PrimitiveCode) {
|
||||
setIsLoading(false)
|
||||
setErrMsg((error as Error).message)
|
||||
}
|
||||
}
|
||||
}, [props.PrimitiveCode])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'neutral',
|
||||
look,
|
||||
startOnLoad: false,
|
||||
fontFamily: 'sans-serif',
|
||||
securityLevel: 'loose',
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
diagramPadding: 10,
|
||||
curve: 'basis',
|
||||
nodeSpacing: 50,
|
||||
rankSpacing: 70,
|
||||
},
|
||||
gantt: {
|
||||
titleTopMargin: 25,
|
||||
barHeight: 20,
|
||||
barGap: 4,
|
||||
topPadding: 50,
|
||||
leftPadding: 75,
|
||||
gridLineStartPadding: 35,
|
||||
fontSize: 11,
|
||||
numberSectionStyles: 4,
|
||||
axisFormat: '%Y-%m-%d',
|
||||
},
|
||||
maxTextSize: 50000,
|
||||
})
|
||||
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
isMermaidInitialized = true
|
||||
}
|
||||
}, [look])
|
||||
catch (error) {
|
||||
console.error('Mermaid initialization error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
return isMermaidInitialized
|
||||
}
|
||||
|
||||
const Flowchart = React.forwardRef((props: {
|
||||
PrimitiveCode: string
|
||||
theme?: 'light' | 'dark'
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const [svgCode, setSvgCode] = useState<string | null>(null)
|
||||
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).substr(2, 9)}`).current
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const renderTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
const [errMsg, setErrMsg] = useState('')
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
const [isCodeComplete, setIsCodeComplete] = useState(false)
|
||||
const codeCompletionCheckRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
// Create cache key from code, style and theme
|
||||
const cacheKey = useMemo(() => {
|
||||
return `${props.PrimitiveCode}-${look}-${currentTheme}`
|
||||
}, [props.PrimitiveCode, look, currentTheme])
|
||||
|
||||
/**
|
||||
* Renders Mermaid chart
|
||||
*/
|
||||
const renderMermaidChart = async (code: string, style: 'classic' | 'handDrawn') => {
|
||||
if (style === 'handDrawn') {
|
||||
// Special handling for hand-drawn style
|
||||
if (containerRef.current)
|
||||
containerRef.current.innerHTML = `<div id="${chartId}"></div>`
|
||||
await new Promise(resolve => setTimeout(resolve, 30))
|
||||
|
||||
if (typeof window !== 'undefined' && mermaidAPI) {
|
||||
// Prefer using mermaidAPI directly for hand-drawn style
|
||||
return await mermaidAPI.render(chartId, code)
|
||||
}
|
||||
else {
|
||||
// Fall back to standard rendering if mermaidAPI is not available
|
||||
const { svg } = await mermaid.render(chartId, code)
|
||||
return { svg }
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Standard rendering for classic style - using the extracted waitForDOMElement function
|
||||
const renderWithRetry = async () => {
|
||||
if (containerRef.current)
|
||||
containerRef.current.innerHTML = `<div id="${chartId}"></div>`
|
||||
await new Promise(resolve => setTimeout(resolve, 30))
|
||||
const { svg } = await mermaid.render(chartId, code)
|
||||
return { svg }
|
||||
}
|
||||
return await waitForDOMElement(renderWithRetry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rendering errors
|
||||
*/
|
||||
const handleRenderError = (error: any) => {
|
||||
console.error('Mermaid rendering error:', error)
|
||||
const errorMsg = (error as Error).message
|
||||
|
||||
if (errorMsg.includes('getAttribute')) {
|
||||
diagramCache.clear()
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
})
|
||||
}
|
||||
else {
|
||||
setErrMsg(`Rendering chart failed, please refresh and try again ${look === 'handDrawn' ? 'Or try using classic mode' : ''}`)
|
||||
}
|
||||
|
||||
if (look === 'handDrawn') {
|
||||
try {
|
||||
// Clear possible cache issues
|
||||
diagramCache.delete(`${props.PrimitiveCode}-handDrawn-${currentTheme}`)
|
||||
|
||||
// Reset mermaid configuration
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
theme: 'default',
|
||||
maxTextSize: 50000,
|
||||
})
|
||||
|
||||
// Try rendering with standard mode
|
||||
setLook('classic')
|
||||
setErrMsg('Hand-drawn mode is not supported for this diagram. Switched to classic mode.')
|
||||
|
||||
// Delay error clearing
|
||||
setTimeout(() => {
|
||||
if (containerRef.current) {
|
||||
// Try rendering again with standard mode, but can't call renderFlowchart directly due to circular dependency
|
||||
// Instead set state to trigger re-render
|
||||
setIsCodeComplete(true) // This will trigger useEffect re-render
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Reset after handDrawn error failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// Initialize mermaid
|
||||
useEffect(() => {
|
||||
if (timeRef.current)
|
||||
window.clearTimeout(timeRef.current)
|
||||
const api = initMermaid()
|
||||
if (api)
|
||||
setIsInitialized(true)
|
||||
}, [])
|
||||
|
||||
timeRef.current = window.setTimeout(() => {
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
// Update theme when prop changes
|
||||
useEffect(() => {
|
||||
if (props.theme)
|
||||
setCurrentTheme(props.theme)
|
||||
}, [props.theme])
|
||||
|
||||
// Validate mermaid code and check for completeness
|
||||
useEffect(() => {
|
||||
if (codeCompletionCheckRef.current)
|
||||
clearTimeout(codeCompletionCheckRef.current)
|
||||
|
||||
// Reset code complete status when code changes
|
||||
setIsCodeComplete(false)
|
||||
|
||||
// If no code or code is extremely short, don't proceed
|
||||
if (!props.PrimitiveCode || props.PrimitiveCode.length < 10)
|
||||
return
|
||||
|
||||
// Check if code already in cache - if so we know it's valid
|
||||
if (diagramCache.has(cacheKey)) {
|
||||
setIsCodeComplete(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Initial check using the extracted isMermaidCodeComplete function
|
||||
const isComplete = isMermaidCodeComplete(props.PrimitiveCode)
|
||||
if (isComplete) {
|
||||
setIsCodeComplete(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Set a delay to check again in case code is still being generated
|
||||
codeCompletionCheckRef.current = setTimeout(() => {
|
||||
setIsCodeComplete(isMermaidCodeComplete(props.PrimitiveCode))
|
||||
}, 300)
|
||||
}, [props.PrimitiveCode])
|
||||
|
||||
return () => {
|
||||
if (codeCompletionCheckRef.current)
|
||||
clearTimeout(codeCompletionCheckRef.current)
|
||||
}
|
||||
}, [props.PrimitiveCode, cacheKey])
|
||||
|
||||
/**
|
||||
* Renders flowchart based on provided code
|
||||
*/
|
||||
const renderFlowchart = useCallback(async (primitiveCode: string) => {
|
||||
if (!isInitialized || !containerRef.current) {
|
||||
setIsLoading(false)
|
||||
setErrMsg(!isInitialized ? 'Mermaid initialization failed' : 'Container element not found')
|
||||
return
|
||||
}
|
||||
|
||||
// Don't render if code is not complete yet
|
||||
if (!isCodeComplete) {
|
||||
setIsLoading(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Return cached result if available
|
||||
if (diagramCache.has(cacheKey)) {
|
||||
setSvgCode(diagramCache.get(cacheKey) || null)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setErrMsg('')
|
||||
|
||||
try {
|
||||
let finalCode: string
|
||||
|
||||
// Check if it's a gantt chart
|
||||
const isGanttChart = primitiveCode.trim().startsWith('gantt')
|
||||
|
||||
if (isGanttChart) {
|
||||
// For gantt charts, ensure each task is on its own line
|
||||
// and preserve exact whitespace/format
|
||||
finalCode = primitiveCode.trim()
|
||||
}
|
||||
else {
|
||||
// Step 1: Clean and prepare Mermaid code using the extracted prepareMermaidCode function
|
||||
finalCode = prepareMermaidCode(primitiveCode, look)
|
||||
}
|
||||
|
||||
// Step 2: Render chart
|
||||
const svgGraph = await renderMermaidChart(finalCode, look)
|
||||
|
||||
// Step 3: Apply theme to SVG using the extracted processSvgForTheme function
|
||||
const processedSvg = processSvgForTheme(
|
||||
svgGraph.svg,
|
||||
currentTheme === Theme.dark,
|
||||
look === 'handDrawn',
|
||||
THEMES,
|
||||
)
|
||||
|
||||
// Step 4: Clean SVG code and convert to base64 using the extracted functions
|
||||
const cleanedSvg = cleanUpSvgCode(processedSvg)
|
||||
const base64Svg = await svgToBase64(cleanedSvg)
|
||||
|
||||
if (base64Svg && typeof base64Svg === 'string') {
|
||||
diagramCache.set(cacheKey, base64Svg)
|
||||
setSvgCode(base64Svg)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
catch (error) {
|
||||
// Error handling
|
||||
handleRenderError(error)
|
||||
}
|
||||
}, [chartId, isInitialized, cacheKey, isCodeComplete, look, currentTheme, t])
|
||||
|
||||
/**
|
||||
* Configure mermaid based on selected style and theme
|
||||
*/
|
||||
const configureMermaid = useCallback(() => {
|
||||
if (typeof window !== 'undefined' && isInitialized) {
|
||||
const themeVars = THEMES[currentTheme]
|
||||
const config: any = {
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
fontFamily: 'sans-serif',
|
||||
maxTextSize: 50000,
|
||||
gantt: {
|
||||
titleTopMargin: 25,
|
||||
barHeight: 20,
|
||||
barGap: 4,
|
||||
topPadding: 50,
|
||||
leftPadding: 75,
|
||||
gridLineStartPadding: 35,
|
||||
fontSize: 11,
|
||||
numberSectionStyles: 4,
|
||||
axisFormat: '%Y-%m-%d',
|
||||
},
|
||||
}
|
||||
|
||||
if (look === 'classic') {
|
||||
config.theme = currentTheme === 'dark' ? 'dark' : 'neutral'
|
||||
config.flowchart = {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
diagramPadding: 12,
|
||||
nodeSpacing: 60,
|
||||
rankSpacing: 80,
|
||||
curve: 'linear',
|
||||
ranker: 'tight-tree',
|
||||
}
|
||||
}
|
||||
else {
|
||||
config.theme = 'default'
|
||||
config.themeCSS = `
|
||||
.node rect { fill-opacity: 0.85; }
|
||||
.edgePath .path { stroke-width: 1.5px; }
|
||||
.label { font-family: 'sans-serif'; }
|
||||
.edgeLabel { font-family: 'sans-serif'; }
|
||||
.cluster rect { rx: 5px; ry: 5px; }
|
||||
`
|
||||
config.themeVariables = {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'sans-serif',
|
||||
}
|
||||
config.flowchart = {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
diagramPadding: 10,
|
||||
nodeSpacing: 40,
|
||||
rankSpacing: 60,
|
||||
curve: 'basis',
|
||||
}
|
||||
config.themeVariables.primaryBorderColor = currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor
|
||||
}
|
||||
|
||||
if (currentTheme === 'dark' && !config.themeVariables) {
|
||||
config.themeVariables = {
|
||||
background: themeVars.background,
|
||||
primaryColor: themeVars.primaryColor,
|
||||
primaryBorderColor: themeVars.primaryBorderColor,
|
||||
primaryTextColor: themeVars.primaryTextColor,
|
||||
secondaryColor: themeVars.secondaryColor,
|
||||
tertiaryColor: themeVars.tertiaryColor,
|
||||
fontFamily: 'sans-serif',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mermaid.initialize(config)
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Config error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, [currentTheme, isInitialized, look])
|
||||
|
||||
// Effect for theme and style configuration
|
||||
useEffect(() => {
|
||||
if (diagramCache.has(cacheKey)) {
|
||||
setSvgCode(diagramCache.get(cacheKey) || null)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (configureMermaid() && containerRef.current && isCodeComplete)
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
}, [look, props.PrimitiveCode, renderFlowchart, isInitialized, cacheKey, currentTheme, isCodeComplete, configureMermaid])
|
||||
|
||||
// Effect for rendering with debounce
|
||||
useEffect(() => {
|
||||
if (diagramCache.has(cacheKey)) {
|
||||
setSvgCode(diagramCache.get(cacheKey) || null)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (renderTimeoutRef.current)
|
||||
clearTimeout(renderTimeoutRef.current)
|
||||
|
||||
if (isCodeComplete) {
|
||||
renderTimeoutRef.current = setTimeout(() => {
|
||||
if (isInitialized)
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
}, 300)
|
||||
}
|
||||
else {
|
||||
setIsLoading(true)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (renderTimeoutRef.current)
|
||||
clearTimeout(renderTimeoutRef.current)
|
||||
}
|
||||
}, [props.PrimitiveCode, renderFlowchart, isInitialized, cacheKey, isCodeComplete])
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (containerRef.current)
|
||||
containerRef.current.innerHTML = ''
|
||||
if (renderTimeoutRef.current)
|
||||
clearTimeout(renderTimeoutRef.current)
|
||||
if (codeCompletionCheckRef.current)
|
||||
clearTimeout(codeCompletionCheckRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setCurrentTheme(prevTheme => prevTheme === 'light' ? Theme.dark : Theme.light)
|
||||
diagramCache.clear()
|
||||
}
|
||||
|
||||
// Style classes for theme-dependent elements
|
||||
const themeClasses = {
|
||||
container: cn('relative', {
|
||||
'bg-white': currentTheme === Theme.light,
|
||||
'bg-slate-900': currentTheme === Theme.dark,
|
||||
}),
|
||||
mermaidDiv: cn('mermaid relative h-auto w-full cursor-pointer', {
|
||||
'bg-white': currentTheme === Theme.light,
|
||||
'bg-slate-900': currentTheme === Theme.dark,
|
||||
}),
|
||||
errorMessage: cn('px-[26px] py-4', {
|
||||
'text-red-500': currentTheme === Theme.light,
|
||||
'text-red-400': currentTheme === Theme.dark,
|
||||
}),
|
||||
errorIcon: cn('h-6 w-6', {
|
||||
'text-red-500': currentTheme === Theme.light,
|
||||
'text-red-400': currentTheme === Theme.dark,
|
||||
}),
|
||||
segmented: cn('msh-segmented msh-segmented-sm css-23bs09 css-var-r1', {
|
||||
'text-gray-700': currentTheme === Theme.light,
|
||||
'text-gray-300': currentTheme === Theme.dark,
|
||||
}),
|
||||
themeToggle: cn('flex h-10 w-10 items-center justify-center rounded-full shadow-md backdrop-blur-sm transition-all duration-300', {
|
||||
'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light,
|
||||
'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark,
|
||||
}),
|
||||
}
|
||||
|
||||
// Style classes for look options
|
||||
const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => {
|
||||
return cn(
|
||||
'system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary',
|
||||
look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300',
|
||||
look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white',
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line ts/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
(<div ref={ref}>
|
||||
<div className="msh-segmented msh-segmented-sm css-23bs09 css-var-r1">
|
||||
<div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}>
|
||||
<div className={themeClasses.segmented}>
|
||||
<div className="msh-segmented-group">
|
||||
<label className="msh-segmented-item m-2 flex w-[200px] items-center space-x-1">
|
||||
<div key='classic'
|
||||
className={cn('system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary',
|
||||
look === 'classic' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
)}
|
||||
|
||||
<div
|
||||
key='classic'
|
||||
className={getLookButtonClass('classic')}
|
||||
onClick={() => setLook('classic')}
|
||||
>
|
||||
<div className="msh-segmented-item-label">{t('app.mermaid.classic')}</div>
|
||||
</div>
|
||||
<div key='handDrawn'
|
||||
className={cn(
|
||||
'system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary',
|
||||
look === 'handDrawn' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
)}
|
||||
<div
|
||||
key='handDrawn'
|
||||
className={getLookButtonClass('handDrawn')}
|
||||
onClick={() => setLook('handDrawn')}
|
||||
>
|
||||
<div className="msh-segmented-item-label">{t('app.mermaid.handDrawn')}</div>
|
||||
@ -118,31 +530,60 @@ const Flowchart = (
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
svgCode
|
||||
&& <div className="mermaid object-fit: cover h-auto w-full cursor-pointer" onClick={() => setImagePreviewUrl(svgCode)}>
|
||||
{svgCode && <img src={svgCode} alt="mermaid_chart" />}
|
||||
|
||||
<div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} />
|
||||
|
||||
{isLoading && !svgCode && (
|
||||
<div className='px-[26px] py-4'>
|
||||
<LoadingAnim type='text'/>
|
||||
{!isCodeComplete && (
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t('common.wait_for_completion', 'Waiting for diagram code to complete...')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{isLoading
|
||||
&& <div className='px-[26px] py-4'>
|
||||
<LoadingAnim type='text' />
|
||||
)}
|
||||
|
||||
{svgCode && (
|
||||
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}>
|
||||
<div className="absolute bottom-2 left-2 z-[100]">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleTheme()
|
||||
}}
|
||||
className={themeClasses.themeToggle}
|
||||
title={(currentTheme === Theme.light ? t('app.theme.switchDark') : t('app.theme.switchLight')) || ''}
|
||||
style={{ transform: 'translate3d(0, 0, 0)' }}
|
||||
>
|
||||
{currentTheme === Theme.light ? <MoonIcon className="h-5 w-5" /> : <SunIcon className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={svgCode}
|
||||
alt="mermaid_chart"
|
||||
style={{ maxWidth: '100%' }}
|
||||
onError={() => { setErrMsg('Chart rendering failed, please refresh and retry') }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
errMsg
|
||||
&& <div className='px-[26px] py-4'>
|
||||
<ExclamationTriangleIcon className='h-6 w-6 text-red-500' />
|
||||
|
||||
{errMsg}
|
||||
)}
|
||||
|
||||
{errMsg && (
|
||||
<div className={themeClasses.errorMessage}>
|
||||
<div className="flex items-center">
|
||||
<ExclamationTriangleIcon className={themeClasses.errorIcon}/>
|
||||
<span className="ml-2">{errMsg}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
imagePreviewUrl && (<ImagePreview title='mermaid_chart' url={imagePreviewUrl} onCancel={() => setImagePreviewUrl('')} />)
|
||||
}
|
||||
</div>)
|
||||
)}
|
||||
|
||||
{imagePreviewUrl && (
|
||||
<ImagePreview title='mermaid_chart' url={imagePreviewUrl} onCancel={() => setImagePreviewUrl('')} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Flowchart.displayName = 'Flowchart'
|
||||
|
||||
|
||||
@ -1,3 +1,236 @@
|
||||
export function cleanUpSvgCode(svgCode: string): string {
|
||||
return svgCode.replaceAll('<br>', '<br/>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses mermaid code to fix common syntax issues
|
||||
*/
|
||||
export function preprocessMermaidCode(code: string): string {
|
||||
if (!code || typeof code !== 'string')
|
||||
return ''
|
||||
|
||||
// First check if this is a gantt chart
|
||||
if (code.trim().startsWith('gantt')) {
|
||||
// For gantt charts, we need to ensure each task is on its own line
|
||||
// Split the code into lines and process each line separately
|
||||
const lines = code.split('\n').map(line => line.trim())
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
return code
|
||||
// Replace English colons with Chinese colons in section nodes to avoid parsing issues
|
||||
.replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}:`)
|
||||
// Fix common syntax issues
|
||||
.replace(/fifopacket/g, 'rect')
|
||||
// Ensure graph has direction
|
||||
.replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
|
||||
return direction ? match : 'graph TD'
|
||||
})
|
||||
// Clean up empty lines and extra spaces
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares mermaid code based on selected style
|
||||
*/
|
||||
export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
|
||||
let finalCode = preprocessMermaidCode(code)
|
||||
|
||||
// Special handling for gantt charts
|
||||
if (finalCode.trim().startsWith('gantt')) {
|
||||
// For gantt charts, preserve the structure exactly as is
|
||||
return finalCode
|
||||
}
|
||||
|
||||
if (style === 'handDrawn') {
|
||||
finalCode = finalCode
|
||||
// Remove style definitions that interfere with hand-drawn style
|
||||
.replace(/style\s+[^\n]+/g, '')
|
||||
.replace(/linkStyle\s+[^\n]+/g, '')
|
||||
.replace(/^flowchart/, 'graph')
|
||||
// Remove any styles that might interfere with hand-drawn style
|
||||
.replace(/class="[^"]*"/g, '')
|
||||
.replace(/fill="[^"]*"/g, '')
|
||||
.replace(/stroke="[^"]*"/g, '')
|
||||
|
||||
// Ensure hand-drawn style charts always start with graph
|
||||
if (!finalCode.startsWith('graph') && !finalCode.startsWith('flowchart'))
|
||||
finalCode = `graph TD\n${finalCode}`
|
||||
}
|
||||
|
||||
return finalCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts SVG to base64 string for image rendering
|
||||
*/
|
||||
export function svgToBase64(svgGraph: string): Promise<string> {
|
||||
if (!svgGraph)
|
||||
return Promise.resolve('')
|
||||
|
||||
try {
|
||||
// Ensure SVG has correct XML declaration
|
||||
if (!svgGraph.includes('<?xml'))
|
||||
svgGraph = `<?xml version="1.0" encoding="UTF-8"?>${svgGraph}`
|
||||
|
||||
const blob = new Blob([new TextEncoder().encode(svgGraph)], { type: 'image/svg+xml;charset=utf-8' })
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result as string)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error converting SVG to base64:', error)
|
||||
return Promise.resolve('')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes SVG for theme styling
|
||||
*/
|
||||
export function processSvgForTheme(
|
||||
svg: string,
|
||||
isDark: boolean,
|
||||
isHandDrawn: boolean,
|
||||
themes: {
|
||||
light: any
|
||||
dark: any
|
||||
},
|
||||
): string {
|
||||
let processedSvg = svg
|
||||
|
||||
if (isDark) {
|
||||
processedSvg = processedSvg
|
||||
.replace(/style="fill: ?#000000"/g, 'style="fill: #e2e8f0"')
|
||||
.replace(/style="stroke: ?#000000"/g, 'style="stroke: #94a3b8"')
|
||||
.replace(/<rect [^>]*fill="#ffffff"/g, '<rect $& fill="#1e293b"')
|
||||
|
||||
if (isHandDrawn) {
|
||||
processedSvg = processedSvg
|
||||
.replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.dark.nodeColors[0].bg}"`)
|
||||
.replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.dark.connectionColor}"`)
|
||||
.replace(/stroke-width="1"/g, 'stroke-width="1.5"')
|
||||
}
|
||||
else {
|
||||
let i = 0
|
||||
themes.dark.nodeColors.forEach(() => {
|
||||
const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
|
||||
processedSvg = processedSvg.replace(regex, (match: string) => {
|
||||
const colorIndex = i % themes.dark.nodeColors.length
|
||||
i++
|
||||
return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`)
|
||||
})
|
||||
})
|
||||
|
||||
processedSvg = processedSvg
|
||||
.replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
|
||||
`<path stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
|
||||
.replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
|
||||
`<$1 stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isHandDrawn) {
|
||||
processedSvg = processedSvg
|
||||
.replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.light.nodeColors[0].bg}"`)
|
||||
.replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.light.connectionColor}"`)
|
||||
.replace(/stroke-width="1"/g, 'stroke-width="1.5"')
|
||||
}
|
||||
else {
|
||||
themes.light.nodeColors.forEach(() => {
|
||||
const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
|
||||
let i = 0
|
||||
processedSvg = processedSvg.replace(regex, (match: string) => {
|
||||
const colorIndex = i % themes.light.nodeColors.length
|
||||
i++
|
||||
return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`)
|
||||
})
|
||||
})
|
||||
|
||||
processedSvg = processedSvg
|
||||
.replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
|
||||
`<path stroke="${themes.light.connectionColor}"`)
|
||||
.replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
|
||||
`<$1 stroke="${themes.light.connectionColor}"`)
|
||||
}
|
||||
}
|
||||
|
||||
return processedSvg
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if mermaid code is complete and valid
|
||||
*/
|
||||
export function isMermaidCodeComplete(code: string): boolean {
|
||||
if (!code || code.trim().length === 0)
|
||||
return false
|
||||
|
||||
try {
|
||||
const trimmedCode = code.trim()
|
||||
|
||||
// Special handling for gantt charts
|
||||
if (trimmedCode.startsWith('gantt')) {
|
||||
// For gantt charts, check if it has at least a title and one task
|
||||
const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
|
||||
return lines.length >= 3
|
||||
}
|
||||
|
||||
// Check for basic syntax structure
|
||||
const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram)/.test(trimmedCode)
|
||||
|
||||
// Check for balanced brackets and parentheses
|
||||
const isBalanced = (() => {
|
||||
const stack = []
|
||||
const pairs = { '{': '}', '[': ']', '(': ')' }
|
||||
|
||||
for (const char of trimmedCode) {
|
||||
if (char in pairs) {
|
||||
stack.push(char)
|
||||
}
|
||||
else if (Object.values(pairs).includes(char)) {
|
||||
const last = stack.pop()
|
||||
if (pairs[last as keyof typeof pairs] !== char)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return stack.length === 0
|
||||
})()
|
||||
|
||||
// Check for common syntax errors
|
||||
const hasNoSyntaxErrors = !trimmedCode.includes('undefined')
|
||||
&& !trimmedCode.includes('[object Object]')
|
||||
&& trimmedCode.split('\n').every(line =>
|
||||
!(line.includes('-->') && !line.match(/\S+\s*-->\s*\S+/)))
|
||||
|
||||
return hasValidStart && isBalanced && hasNoSyntaxErrors
|
||||
}
|
||||
catch (error) {
|
||||
console.debug('Mermaid code validation error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wait for DOM element with retry mechanism
|
||||
*/
|
||||
export function waitForDOMElement(callback: () => Promise<any>, maxAttempts = 3, delay = 100): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let attempts = 0
|
||||
const tryRender = async () => {
|
||||
try {
|
||||
resolve(await callback())
|
||||
}
|
||||
catch (error) {
|
||||
attempts++
|
||||
if (attempts < maxAttempts)
|
||||
setTimeout(tryRender, delay)
|
||||
else
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
tryRender()
|
||||
})
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import { useOptions } from './hooks'
|
||||
import type { PickerBlockMenuOption } from './menu'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { KEY_ESCAPE_COMMAND } from 'lexical'
|
||||
|
||||
type ComponentPickerProps = {
|
||||
triggerString: string
|
||||
@ -118,6 +119,13 @@ const ComponentPicker = ({
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
|
||||
}, [editor, checkForTriggerMatch, triggerString])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent)
|
||||
})
|
||||
}, [editor])
|
||||
|
||||
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
|
||||
anchorElementRef,
|
||||
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
@ -141,51 +149,54 @@ const ComponentPicker = ({
|
||||
visibility: isPositioned ? 'visible' : 'hidden',
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
data-testid="component-picker-container"
|
||||
>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
// Divider
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<>
|
||||
{
|
||||
(!!options.length) && (
|
||||
<div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass='max-h-[34vh]'
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div className='p-1'>
|
||||
<VarReferenceVars
|
||||
searchBoxClassName='mt-1'
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass='max-h-[34vh]'
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && !!options.length && (
|
||||
<div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
<div data-testid="options-list">
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
// Divider
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
@ -193,7 +204,7 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable])
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
|
||||
@ -37,14 +37,16 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
|
||||
),
|
||||
editor.registerCommand(
|
||||
BLUR_COMMAND,
|
||||
() => {
|
||||
ref.current = setTimeout(() => {
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
}, 200)
|
||||
|
||||
if (onBlur)
|
||||
onBlur()
|
||||
|
||||
(event) => {
|
||||
// Check if the clicked target element is var-search-input
|
||||
const target = event?.relatedTarget as HTMLElement
|
||||
if (!target?.classList?.contains('var-search-input')) {
|
||||
ref.current = setTimeout(() => {
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
}, 200)
|
||||
if (onBlur)
|
||||
onBlur()
|
||||
}
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
|
||||
@ -150,8 +150,8 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
||||
</div>
|
||||
</div>
|
||||
{isPartialMembers && (
|
||||
<div className='max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular p-1'>
|
||||
<div className='sticky left-0 top-0 p-2 pb-1'>
|
||||
<div className='max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular pb-1 pl-1 pr-1'>
|
||||
<div className='sticky left-0 top-0 z-10 bg-white p-2 pb-1'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
|
||||
@ -74,7 +74,7 @@ const Popup: FC<PopupProps> = ({
|
||||
/>
|
||||
<input
|
||||
className='block h-[18px] grow appearance-none bg-transparent text-[13px] text-text-primary outline-none'
|
||||
placeholder='Search model'
|
||||
placeholder={t('datasetSettings.form.searchModel') || ''}
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
/>
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
'use client'
|
||||
import { useMemo } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Collection } from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
|
||||
|
||||
type Props = {
|
||||
active: boolean
|
||||
collection: Collection
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
const ProviderCard = ({
|
||||
active,
|
||||
collection,
|
||||
onSelect,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const labelList = useLabelStore(s => s.labelList)
|
||||
|
||||
const labelContent = useMemo(() => {
|
||||
if (!collection.labels)
|
||||
return ''
|
||||
return collection.labels.map((name) => {
|
||||
const label = labelList.find(item => item.name === name)
|
||||
return label?.label[language]
|
||||
}).filter(Boolean).join(', ')
|
||||
}, [collection.labels, labelList, language])
|
||||
|
||||
return (
|
||||
<div className={cn('group col-span-1 flex min-h-[160px] cursor-pointer flex-col rounded-xl border-2 border-solid border-transparent bg-white shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg', active && '!border-primary-400')} onClick={onSelect}>
|
||||
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
|
||||
<div className='relative shrink-0'>
|
||||
{typeof collection.icon === 'string' && (
|
||||
<div className='h-10 w-10 rounded-md bg-cover bg-center bg-no-repeat' style={{ backgroundImage: `url(${collection.icon})` }} />
|
||||
)}
|
||||
{typeof collection.icon !== 'string' && (
|
||||
<AppIcon
|
||||
size='large'
|
||||
icon={collection.icon.content}
|
||||
background={collection.icon.background}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='w-0 grow py-[1px]'>
|
||||
<div className='flex items-center text-sm font-semibold leading-5 text-gray-800'>
|
||||
<div className='truncate' title={collection.label[language]}>{collection.label[language]}</div>
|
||||
</div>
|
||||
<div className='flex items-center text-[10px] font-medium leading-[18px] text-gray-500'>
|
||||
<div className='truncate'>{t('tools.author')} {collection.author}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 max-h-[72px] grow px-[14px] text-xs leading-normal text-gray-500',
|
||||
collection.labels?.length ? 'line-clamp-2' : 'line-clamp-4',
|
||||
collection.labels?.length > 0 && 'group-hover:line-clamp-2 group-hover:max-h-[36px]',
|
||||
)}
|
||||
title={collection.description[language]}
|
||||
>
|
||||
{collection.description[language]}
|
||||
</div>
|
||||
{collection.labels?.length > 0 && (
|
||||
<div className='mt-1 flex h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1'>
|
||||
<div className='relative flex w-full items-center gap-1 rounded-md py-[7px] text-gray-500' title={labelContent}>
|
||||
<Tag01 className='h-3 w-3 shrink-0' />
|
||||
<div className='grow truncate text-start text-xs font-normal leading-[18px]'>{labelContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ProviderCard
|
||||
@ -1,40 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiHammerFill,
|
||||
} from '@remixicon/react'
|
||||
import { Heart02 } from '@/app/components/base/icons/src/vender/solid/education'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const Contribute: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<a
|
||||
href='https://github.com/langgenius/dify/blob/main/api/core/tools/README.md'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="group col-span-1 flex min-h-[160px] cursor-pointer flex-col rounded-xl border-2 border-solid border-transparent bg-white bg-[url('~@/app/components/tools/provider/grid_bg.svg')] bg-cover bg-no-repeat shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
|
||||
>
|
||||
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
|
||||
<div className='relative flex shrink-0 items-center'>
|
||||
<div className='z-10 flex rounded-[10px] border-[0.5px] border-primary-100 bg-white p-3 shadow-md'><RiHammerFill className='h-4 w-4 text-primary-600'/></div>
|
||||
<div className='flex -translate-x-2 rounded-[10px] border-[0.5px] border-[#FCE7F6] bg-[#FEF6FB] p-3 shadow-md'><Heart02 className='h-4 w-4 text-[#EE46BC]'/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-3 px-[14px] text-[15px] font-semibold leading-5'>
|
||||
<div className='text-gradient'>{t('tools.contribute.line1')}</div>
|
||||
<div className='text-gradient'>{t('tools.contribute.line2')}</div>
|
||||
</div>
|
||||
<div className='flex items-center space-x-1 border-t-[0.5px] border-black/5 px-4 py-3 text-[#155EEF]'>
|
||||
<BookOpen01 className='h-3 w-3' />
|
||||
<div className='grow text-xs font-normal leading-[18px]'>{t('tools.contribute.viewGuide')}</div>
|
||||
<ArrowUpRight className='h-3 w-3' />
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
export default React.memo(Contribute)
|
||||
@ -236,27 +236,29 @@ const ProviderDetail = ({
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')}
|
||||
>
|
||||
<div className='p-4'>
|
||||
<div className='mb-3 flex'>
|
||||
<Icon src={collection.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<Title title={collection.label[language]} />
|
||||
<div className='flex h-full flex-col p-4'>
|
||||
<div className="shrink-0">
|
||||
<div className='mb-3 flex'>
|
||||
<Icon src={collection.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<Title title={collection.label[language]} />
|
||||
</div>
|
||||
<div className='mb-1 flex h-4 items-center justify-between'>
|
||||
<OrgInfo
|
||||
className="mt-0.5"
|
||||
packageNameClassName='w-auto'
|
||||
orgName={collection.author}
|
||||
packageName={collection.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-1 flex h-4 items-center justify-between'>
|
||||
<OrgInfo
|
||||
className="mt-0.5"
|
||||
packageNameClassName='w-auto'
|
||||
orgName={collection.author}
|
||||
packageName={collection.name}
|
||||
/>
|
||||
<div className='flex gap-1'>
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-1'>
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{!!collection.description[language] && (
|
||||
<Description text={collection.description[language]} descriptionLineRows={2}></Description>
|
||||
@ -292,85 +294,84 @@ const ProviderDetail = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Tools */}
|
||||
<div className='pt-3'>
|
||||
<div className='flex min-h-0 flex-1 flex-col pt-3'>
|
||||
{isDetailLoading && <div className='flex h-[200px]'><Loading type='app' /></div>}
|
||||
{/* Builtin type */}
|
||||
{!isDetailLoading && (collection.type === CollectionType.builtIn) && isAuthed && (
|
||||
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
|
||||
{t('plugin.detailPanel.actionNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
|
||||
{needAuth && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isDetailLoading && (collection.type === CollectionType.builtIn) && needAuth && !isAuthed && (
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
<span className=''>{t('tools.includeToolNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
<span className='px-1'>·</span>
|
||||
<span className='text-util-colors-orange-orange-600'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className={cn('my-3 w-full shrink-0')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.auth.unauthorized')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* Custom type */}
|
||||
{!isDetailLoading && (collection.type === CollectionType.custom) && (
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
<span className=''>{t('tools.includeToolNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Workflow type */}
|
||||
{!isDetailLoading && (collection.type === CollectionType.workflow) && (
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
<span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{!isDetailLoading && (
|
||||
<div className='mt-1 py-2'>
|
||||
{collection.type !== CollectionType.workflow && toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={tool.name}
|
||||
disabled={false}
|
||||
// disabled={needAuth && (isBuiltIn || isModel) && !isAuthed}
|
||||
collection={collection}
|
||||
tool={tool}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
/>
|
||||
))}
|
||||
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
|
||||
<div key={item.name} className='mb-1 py-1'>
|
||||
<div className='mb-1 flex items-center gap-2'>
|
||||
<span className='code-sm-semibold text-text-secondary'>{item.name}</span>
|
||||
<span className='system-xs-regular text-text-tertiary'>{item.type}</span>
|
||||
<span className='system-xs-medium text-text-warning-secondary'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
|
||||
<>
|
||||
<div className="shrink-0">
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && isAuthed && (
|
||||
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
|
||||
{t('plugin.detailPanel.actionNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
|
||||
{needAuth && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{item.llm_description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && !isAuthed && (
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
<span className=''>{t('tools.includeToolNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
<span className='px-1'>·</span>
|
||||
<span className='text-util-colors-orange-orange-600'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className={cn('my-3 w-full shrink-0')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.auth.unauthorized')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(collection.type === CollectionType.custom) && (
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
<span className=''>{t('tools.includeToolNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{(collection.type === CollectionType.workflow) && (
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
<span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-1 flex-1 overflow-y-auto py-2'>
|
||||
{collection.type !== CollectionType.workflow && toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={tool.name}
|
||||
disabled={false}
|
||||
collection={collection}
|
||||
tool={tool}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
/>
|
||||
))}
|
||||
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
|
||||
<div key={item.name} className='mb-1 py-1'>
|
||||
<div className='mb-1 flex items-center gap-2'>
|
||||
<span className='code-sm-semibold text-text-secondary'>{item.name}</span>
|
||||
<span className='system-xs-regular text-text-tertiary'>{item.type}</span>
|
||||
<span className='system-xs-medium text-text-warning-secondary'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{item.llm_description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@ -7,6 +7,7 @@ import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import WorkflowChildren from './workflow-children'
|
||||
import {
|
||||
useAvailableNodesMetaData,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowRun,
|
||||
useWorkflowStartRun,
|
||||
@ -44,6 +45,7 @@ const WorkflowMain = ({
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
} = useWorkflowStartRun()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
@ -57,6 +59,7 @@ const WorkflowMain = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
availableNodesMetaData,
|
||||
}
|
||||
}, [
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
@ -69,6 +72,7 @@ const WorkflowMain = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
availableNodesMetaData,
|
||||
])
|
||||
|
||||
return (
|
||||
@ -77,7 +81,7 @@ const WorkflowMain = ({
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
onWorkflowDataUpdate={handleWorkflowDataUpdate}
|
||||
hooksStore={hooksStore}
|
||||
hooksStore={hooksStore as any}
|
||||
>
|
||||
<WorkflowChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
|
||||
@ -4,3 +4,4 @@ export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-is-chat-mode'
|
||||
export * from './use-available-nodes-meta-data'
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import StartDefault from '@/app/components/workflow/nodes/start/default'
|
||||
import EndDefault from '@/app/components/workflow/nodes/end/default'
|
||||
import AnswerDefault from '@/app/components/workflow/nodes/answer/default'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
|
||||
import { useIsChatMode } from './use-is-chat-mode'
|
||||
|
||||
export const useAvailableNodesMetaData = () => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
const language = useGetLanguage()
|
||||
|
||||
console.log('isChatMode', isChatMode)
|
||||
|
||||
const mergedNodesMetaData = useMemo(() => [
|
||||
...WORKFLOW_COMMON_NODES,
|
||||
StartDefault,
|
||||
...(
|
||||
isChatMode
|
||||
? [AnswerDefault]
|
||||
: [EndDefault]
|
||||
),
|
||||
], [isChatMode])
|
||||
|
||||
const prefixLink = useMemo(() => {
|
||||
if (language === 'zh_Hans')
|
||||
return 'https://docs.dify.ai/zh-hans/guides/workflow/node/'
|
||||
|
||||
return 'https://docs.dify.ai/guides/workflow/node/'
|
||||
}, [language])
|
||||
|
||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
defaultValue: {
|
||||
...node.defaultValue,
|
||||
type: node.type,
|
||||
},
|
||||
title: t(`workflow.blocks.${node.type}`),
|
||||
description: t(`workflow.blocksAbout.${node.type}`),
|
||||
helpLinkUri: `${prefixLink}${node.helpLinkUri}`,
|
||||
|
||||
}
|
||||
}), [mergedNodesMetaData, t, prefixLink])
|
||||
|
||||
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
|
||||
acc![node.type] = node
|
||||
return acc
|
||||
}, {} as AvailableNodesMetaData['nodesMap']), [availableNodesMetaData])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
nodes: availableNodesMetaData,
|
||||
nodesMap: availableNodesMetaDataMap,
|
||||
}
|
||||
}, [availableNodesMetaData, availableNodesMetaDataMap])
|
||||
}
|
||||
@ -3,15 +3,17 @@ import {
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import { useNodesInitialData } from '@/app/components/workflow/hooks'
|
||||
import { useIsChatMode } from './use-is-chat-mode'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import startDefault from '@/app/components/workflow/nodes/start/default'
|
||||
import llmDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import answerDefault from '@/app/components/workflow/nodes/answer/default'
|
||||
|
||||
export const useWorkflowTemplate = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const nodesInitialData = useNodesInitialData()
|
||||
|
||||
const { newNode: startNode } = generateNewNode({
|
||||
data: nodesInitialData.start,
|
||||
data: startDefault.defaultValue as StartNodeType,
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
@ -19,7 +21,7 @@ export const useWorkflowTemplate = () => {
|
||||
const { newNode: llmNode } = generateNewNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
...nodesInitialData.llm,
|
||||
...llmDefault.defaultValue,
|
||||
memory: {
|
||||
window: { enabled: false, size: 10 },
|
||||
query_prompt_template: '{{#sys.query#}}',
|
||||
@ -35,7 +37,7 @@ export const useWorkflowTemplate = () => {
|
||||
const { newNode: answerNode } = generateNewNode({
|
||||
id: 'answer',
|
||||
data: {
|
||||
...nodesInitialData.answer,
|
||||
...answerDefault.defaultValue,
|
||||
answer: `{{#${llmNode.id}.text#}}`,
|
||||
},
|
||||
position: {
|
||||
|
||||
@ -7,12 +7,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { groupBy } from 'lodash-es'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesExtraData,
|
||||
} from '../hooks'
|
||||
import type { NodeDefault } from '../types'
|
||||
import { BLOCK_CLASSIFICATIONS } from './constants'
|
||||
import { useBlocks } from './hooks'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
@ -21,23 +17,19 @@ type BlocksProps = {
|
||||
searchText: string
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
blocks: NodeDefault[]
|
||||
}
|
||||
const Blocks = ({
|
||||
searchText,
|
||||
onSelect,
|
||||
availableBlocksTypes = [],
|
||||
blocks,
|
||||
}: BlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const blocks = useBlocks()
|
||||
|
||||
const groups = useMemo(() => {
|
||||
return BLOCK_CLASSIFICATIONS.reduce((acc, classification) => {
|
||||
const list = groupBy(blocks, 'classification')[classification].filter((block) => {
|
||||
if (block.type === BlockEnum.Answer && !isChatMode)
|
||||
return false
|
||||
|
||||
return block.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.type)
|
||||
})
|
||||
|
||||
@ -46,11 +38,11 @@ const Blocks = ({
|
||||
[classification]: list,
|
||||
}
|
||||
}, {} as Record<string, typeof blocks>)
|
||||
}, [blocks, isChatMode, searchText, availableBlocksTypes])
|
||||
}, [blocks, searchText, availableBlocksTypes])
|
||||
const isEmpty = Object.values(groups).every(list => !list.length)
|
||||
|
||||
const renderGroup = useCallback((classification: string) => {
|
||||
const list = groups[classification]
|
||||
const list = groups[classification].sort((a, b) => a.sort - b.sort)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -78,7 +70,7 @@ const Blocks = ({
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='system-md-medium mb-1 text-text-primary'>{block.title}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{nodesExtraData[block.type].about}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{block.description}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
@ -106,7 +98,7 @@ const Blocks = ({
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}, [groups, nodesExtraData, onSelect, t])
|
||||
}, [groups, onSelect, t])
|
||||
|
||||
return (
|
||||
<div className='max-h-[480px] overflow-y-auto p-1'>
|
||||
|
||||
@ -1,113 +1,5 @@
|
||||
import type { Block } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { BlockClassificationEnum } from './types'
|
||||
|
||||
export const BLOCKS: Block[] = [
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.DataSource,
|
||||
title: 'File upload',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Start,
|
||||
title: 'Start',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.KnowledgeRetrieval,
|
||||
title: 'Knowledge Retrieval',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.End,
|
||||
title: 'End',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Answer,
|
||||
title: 'Direct Answer',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.QuestionUnderstand,
|
||||
type: BlockEnum.QuestionClassifier,
|
||||
title: 'Question Classifier',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.IfElse,
|
||||
title: 'IF/ELSE',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: 'Exit Loop',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.Iteration,
|
||||
title: 'Iteration',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.Loop,
|
||||
title: 'Loop',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
type: BlockEnum.Code,
|
||||
title: 'Code',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
type: BlockEnum.TemplateTransform,
|
||||
title: 'Templating Transform',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
type: BlockEnum.VariableAggregator,
|
||||
title: 'Variable Aggregator',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
type: BlockEnum.DocExtractor,
|
||||
title: 'Doc Extractor',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
type: BlockEnum.Assigner,
|
||||
title: 'Variable Assigner',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
title: 'Parameter Extractor',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
type: BlockEnum.HttpRequest,
|
||||
title: 'HTTP Request',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
type: BlockEnum.ListFilter,
|
||||
title: 'List Filter',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Agent',
|
||||
},
|
||||
]
|
||||
|
||||
export const BLOCK_CLASSIFICATIONS: string[] = [
|
||||
BlockClassificationEnum.Default,
|
||||
BlockClassificationEnum.QuestionUnderstand,
|
||||
|
||||
@ -3,23 +3,11 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BLOCKS } from './constants'
|
||||
import {
|
||||
TabsEnum,
|
||||
ToolTypeEnum,
|
||||
} from './types'
|
||||
|
||||
export const useBlocks = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return BLOCKS.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
title: t(`workflow.blocks.${block.type}`),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useTabs = (noBlocks?: boolean) => {
|
||||
const { t } = useTranslation()
|
||||
const tabs = useMemo(() => {
|
||||
|
||||
@ -1,199 +1,36 @@
|
||||
import type {
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import type { BlockEnum, OnSelectBlock } from '../types'
|
||||
import Tabs from './tabs'
|
||||
import { TabsEnum } from './types'
|
||||
import { useTabs } from './hooks'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Input from '@/app/components/base/input'
|
||||
// import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useMemo } from 'react'
|
||||
import type { NodeSelectorProps } from './main'
|
||||
import NodeSelector from './main'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
import {
|
||||
Plus02,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
const NodeSelectorWrapper = (props: NodeSelectorProps) => {
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
|
||||
type NodeSelectorProps = {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSelect: OnSelectBlock
|
||||
trigger?: (open: boolean) => React.ReactNode
|
||||
placement?: Placement
|
||||
offset?: OffsetOptions
|
||||
triggerStyle?: React.CSSProperties
|
||||
triggerClassName?: (open: boolean) => string
|
||||
triggerInnerClassName?: string
|
||||
popupClassName?: string
|
||||
asChild?: boolean
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
disabled?: boolean
|
||||
noBlocks?: boolean
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
trigger,
|
||||
placement = 'right',
|
||||
offset = 6,
|
||||
triggerClassName,
|
||||
triggerInnerClassName,
|
||||
triggerStyle,
|
||||
popupClassName,
|
||||
asChild,
|
||||
availableBlocksTypes,
|
||||
disabled,
|
||||
noBlocks = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const [localOpen, setLocalOpen] = useState(false)
|
||||
const open = openFromProps === undefined ? localOpen : openFromProps
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setLocalOpen(newOpen)
|
||||
const blocks = useMemo(() => {
|
||||
const result = availableNodesMetaData?.nodes || []
|
||||
console.log(result, 'result')
|
||||
|
||||
if (!newOpen)
|
||||
setSearchText('')
|
||||
return result.filter((block) => {
|
||||
if (block.type === BlockEnum.Start)
|
||||
return false
|
||||
|
||||
if (onOpenChange)
|
||||
onOpenChange(newOpen)
|
||||
}, [onOpenChange])
|
||||
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
|
||||
if (disabled)
|
||||
return
|
||||
e.stopPropagation()
|
||||
handleOpenChange(!open)
|
||||
}, [handleOpenChange, open, disabled])
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleOpenChange(false)
|
||||
onSelect(type, toolDefaultValue)
|
||||
}, [handleOpenChange, onSelect])
|
||||
if (block.type === BlockEnum.IterationStart)
|
||||
return false
|
||||
|
||||
const {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
tabs,
|
||||
} = useTabs()
|
||||
if (block.type === BlockEnum.LoopStart)
|
||||
return false
|
||||
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (activeTab === TabsEnum.Blocks)
|
||||
return t('workflow.tabs.searchBlock')
|
||||
|
||||
if (activeTab === TabsEnum.Tools)
|
||||
return t('workflow.tabs.searchTool')
|
||||
return ''
|
||||
}, [activeTab, t])
|
||||
return true
|
||||
})
|
||||
}, [availableNodesMetaData?.nodes])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement={placement}
|
||||
offset={offset}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
asChild={asChild}
|
||||
onClick={handleTrigger}
|
||||
className={triggerInnerClassName}
|
||||
>
|
||||
{
|
||||
trigger
|
||||
? trigger(open)
|
||||
: (
|
||||
<div
|
||||
className={`
|
||||
z-10 flex h-4
|
||||
w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
|
||||
${triggerClassName?.(open)}
|
||||
`}
|
||||
style={triggerStyle}
|
||||
>
|
||||
<Plus02 className='h-2.5 w-2.5' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={cn(
|
||||
'overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-[5px]',
|
||||
popupClassName,
|
||||
)}>
|
||||
<div className='border-b border-divider-subtle bg-background-section-burn'>
|
||||
<div className='flex h-9 items-center px-1 pt-1'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
'system-sm-medium mr-0.5 cursor-pointer rounded-t-lg px-3 py-2 text-text-tertiary hover:bg-state-base-hover',
|
||||
activeTab === tab.key && 'bg-components-panel-bg text-text-accent shadow-sm',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setActiveTab(tab.key)
|
||||
}}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='relative z-[1] bg-components-panel-bg p-2'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
autoFocus
|
||||
value={searchText}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className='p-2' onClick={e => e.stopPropagation()}>
|
||||
{activeTab === TabsEnum.Blocks && (
|
||||
)}
|
||||
{activeTab === TabsEnum.Tools && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
/>
|
||||
)}
|
||||
</div> */}
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onSelect={handleSelect}
|
||||
searchText={searchText}
|
||||
tags={tags}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
noBlocks={noBlocks}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
<NodeSelector
|
||||
{...props}
|
||||
blocks={blocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeSelector)
|
||||
export default NodeSelectorWrapper
|
||||
|
||||
175
web/app/components/workflow/block-selector/main.tsx
Normal file
175
web/app/components/workflow/block-selector/main.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import type {
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
OnSelectBlock,
|
||||
} from '../types'
|
||||
import Tabs from './tabs'
|
||||
import { TabsEnum } from './types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Input from '@/app/components/base/input'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
|
||||
import {
|
||||
Plus02,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
export type NodeSelectorProps = {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSelect: OnSelectBlock
|
||||
trigger?: (open: boolean) => React.ReactNode
|
||||
placement?: Placement
|
||||
offset?: OffsetOptions
|
||||
triggerStyle?: React.CSSProperties
|
||||
triggerClassName?: (open: boolean) => string
|
||||
triggerInnerClassName?: string
|
||||
popupClassName?: string
|
||||
asChild?: boolean
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
disabled?: boolean
|
||||
blocks?: NodeDefault[]
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
trigger,
|
||||
placement = 'right',
|
||||
offset = 6,
|
||||
triggerClassName,
|
||||
triggerInnerClassName,
|
||||
triggerStyle,
|
||||
popupClassName,
|
||||
asChild,
|
||||
availableBlocksTypes,
|
||||
disabled,
|
||||
blocks = [],
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const [localOpen, setLocalOpen] = useState(false)
|
||||
const open = openFromProps === undefined ? localOpen : openFromProps
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setLocalOpen(newOpen)
|
||||
|
||||
if (!newOpen)
|
||||
setSearchText('')
|
||||
|
||||
if (onOpenChange)
|
||||
onOpenChange(newOpen)
|
||||
}, [onOpenChange])
|
||||
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
|
||||
if (disabled)
|
||||
return
|
||||
e.stopPropagation()
|
||||
handleOpenChange(!open)
|
||||
}, [handleOpenChange, open, disabled])
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleOpenChange(false)
|
||||
onSelect(type, toolDefaultValue)
|
||||
}, [handleOpenChange, onSelect])
|
||||
|
||||
const [activeTab, setActiveTab] = useState(!blocks.length ? TabsEnum.Tools : TabsEnum.Blocks)
|
||||
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
|
||||
setActiveTab(newActiveTab)
|
||||
}, [])
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (activeTab === TabsEnum.Blocks)
|
||||
return t('workflow.tabs.searchBlock')
|
||||
|
||||
if (activeTab === TabsEnum.Tools)
|
||||
return t('workflow.tabs.searchTool')
|
||||
return ''
|
||||
}, [activeTab, t])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement={placement}
|
||||
offset={offset}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
asChild={asChild}
|
||||
onClick={handleTrigger}
|
||||
className={triggerInnerClassName}
|
||||
>
|
||||
{
|
||||
trigger
|
||||
? trigger(open)
|
||||
: (
|
||||
<div
|
||||
className={`
|
||||
z-10 flex h-4
|
||||
w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
|
||||
${triggerClassName?.(open)}
|
||||
`}
|
||||
style={triggerStyle}
|
||||
>
|
||||
<Plus02 className='h-2.5 w-2.5' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
|
||||
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
|
||||
{activeTab === TabsEnum.Blocks && (
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
autoFocus
|
||||
value={searchText}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === TabsEnum.Tools && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
onSelect={handleSelect}
|
||||
searchText={searchText}
|
||||
tags={tags}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
blocks={blocks}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeSelector)
|
||||
@ -1,7 +1,10 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import type { BlockEnum } from '../types'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
} from '../types'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import { TabsEnum } from './types'
|
||||
import Blocks from './blocks'
|
||||
@ -13,7 +16,7 @@ export type TabsProps = {
|
||||
tags: string[]
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
noBlocks?: boolean
|
||||
blocks: NodeDefault[]
|
||||
}
|
||||
const Tabs: FC<TabsProps> = ({
|
||||
activeTab,
|
||||
@ -21,7 +24,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
searchText,
|
||||
onSelect,
|
||||
availableBlocksTypes,
|
||||
noBlocks,
|
||||
blocks,
|
||||
}) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
@ -30,11 +33,12 @@ const Tabs: FC<TabsProps> = ({
|
||||
return (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
activeTab === TabsEnum.Blocks && !noBlocks && (
|
||||
activeTab === TabsEnum.Blocks && !!blocks.length && (
|
||||
<Blocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
blocks={blocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { BlockEnum } from './types'
|
||||
|
||||
export const ALL_AVAILABLE_BLOCKS = Object.values(BlockEnum)
|
||||
export const ALL_CHAT_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
|
||||
export const ALL_COMPLETION_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]
|
||||
@ -1,421 +1,5 @@
|
||||
import type { Var } from './types'
|
||||
import { BlockEnum, VarType } from './types'
|
||||
import StartNodeDefault from './nodes/start/default'
|
||||
import AnswerDefault from './nodes/answer/default'
|
||||
import LLMDefault from './nodes/llm/default'
|
||||
import KnowledgeRetrievalDefault from './nodes/knowledge-retrieval/default'
|
||||
import QuestionClassifierDefault from './nodes/question-classifier/default'
|
||||
import IfElseDefault from './nodes/if-else/default'
|
||||
import CodeDefault from './nodes/code/default'
|
||||
import TemplateTransformDefault from './nodes/template-transform/default'
|
||||
import HttpRequestDefault from './nodes/http/default'
|
||||
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
|
||||
import ToolDefault from './nodes/tool/default'
|
||||
import VariableAssignerDefault from './nodes/variable-assigner/default'
|
||||
import AssignerDefault from './nodes/assigner/default'
|
||||
import EndNodeDefault from './nodes/end/default'
|
||||
import IterationDefault from './nodes/iteration/default'
|
||||
import LoopDefault from './nodes/loop/default'
|
||||
import DocExtractorDefault from './nodes/document-extractor/default'
|
||||
import ListFilterDefault from './nodes/list-operator/default'
|
||||
import IterationStartDefault from './nodes/iteration-start/default'
|
||||
import AgentDefault from './nodes/agent/default'
|
||||
import LoopStartDefault from './nodes/loop-start/default'
|
||||
import LoopEndDefault from './nodes/loop-end/default'
|
||||
import DataSourceDefault from './nodes/data-source/default'
|
||||
|
||||
type NodesExtraData = {
|
||||
author: string
|
||||
about: string
|
||||
availablePrevNodes: BlockEnum[]
|
||||
availableNextNodes: BlockEnum[]
|
||||
getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[]
|
||||
getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[]
|
||||
checkValid: any
|
||||
}
|
||||
export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
|
||||
[BlockEnum.DataSource]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: DataSourceDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: DataSourceDefault.getAvailableNextNodes,
|
||||
checkValid: DataSourceDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Start]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: StartNodeDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: StartNodeDefault.getAvailableNextNodes,
|
||||
checkValid: StartNodeDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.End]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: EndNodeDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: EndNodeDefault.getAvailableNextNodes,
|
||||
checkValid: EndNodeDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Answer]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: AnswerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: AnswerDefault.getAvailableNextNodes,
|
||||
checkValid: AnswerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LLM]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LLMDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LLMDefault.getAvailableNextNodes,
|
||||
checkValid: LLMDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.KnowledgeRetrieval]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: KnowledgeRetrievalDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: KnowledgeRetrievalDefault.getAvailableNextNodes,
|
||||
checkValid: KnowledgeRetrievalDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.IfElse]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IfElseDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IfElseDefault.getAvailableNextNodes,
|
||||
checkValid: IfElseDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Iteration]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IterationDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IterationDefault.getAvailableNextNodes,
|
||||
checkValid: IterationDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.IterationStart]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IterationStartDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IterationStartDefault.getAvailableNextNodes,
|
||||
checkValid: IterationStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Loop]: {
|
||||
author: 'AICT-Team',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopDefault.getAvailableNextNodes,
|
||||
checkValid: LoopDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LoopStart]: {
|
||||
author: 'AICT-Team',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopStartDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes,
|
||||
checkValid: LoopStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopEndDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopEndDefault.getAvailableNextNodes,
|
||||
checkValid: LoopEndDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: CodeDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: CodeDefault.getAvailableNextNodes,
|
||||
checkValid: CodeDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.TemplateTransform]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: TemplateTransformDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: TemplateTransformDefault.getAvailableNextNodes,
|
||||
checkValid: TemplateTransformDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.QuestionClassifier]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: QuestionClassifierDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: QuestionClassifierDefault.getAvailableNextNodes,
|
||||
checkValid: QuestionClassifierDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.HttpRequest]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: HttpRequestDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: HttpRequestDefault.getAvailableNextNodes,
|
||||
checkValid: HttpRequestDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.VariableAssigner]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: VariableAssignerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
|
||||
checkValid: VariableAssignerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Assigner]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: AssignerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: AssignerDefault.getAvailableNextNodes,
|
||||
checkValid: AssignerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.VariableAggregator]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: VariableAssignerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
|
||||
checkValid: VariableAssignerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.ParameterExtractor]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ParameterExtractorDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ParameterExtractorDefault.getAvailableNextNodes,
|
||||
checkValid: ParameterExtractorDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Tool]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ToolDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ToolDefault.getAvailableNextNodes,
|
||||
checkValid: ToolDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.DocExtractor]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: DocExtractorDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: DocExtractorDefault.getAvailableNextNodes,
|
||||
checkValid: DocExtractorDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.ListFilter]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ListFilterDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes,
|
||||
checkValid: ListFilterDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Agent]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ListFilterDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes,
|
||||
checkValid: AgentDefault.checkValid,
|
||||
},
|
||||
}
|
||||
|
||||
export const NODES_INITIAL_DATA = {
|
||||
[BlockEnum.DataSource]: {
|
||||
type: BlockEnum.DataSource,
|
||||
title: '',
|
||||
desc: '',
|
||||
...DataSourceDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Start]: {
|
||||
type: BlockEnum.Start,
|
||||
title: '',
|
||||
desc: '',
|
||||
...StartNodeDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.End]: {
|
||||
type: BlockEnum.End,
|
||||
title: '',
|
||||
desc: '',
|
||||
...EndNodeDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Answer]: {
|
||||
type: BlockEnum.Answer,
|
||||
title: '',
|
||||
desc: '',
|
||||
...AnswerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LLM]: {
|
||||
type: BlockEnum.LLM,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
...LLMDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.KnowledgeRetrieval]: {
|
||||
type: BlockEnum.KnowledgeRetrieval,
|
||||
title: '',
|
||||
desc: '',
|
||||
query_variable_selector: [],
|
||||
dataset_ids: [],
|
||||
retrieval_mode: 'single',
|
||||
...KnowledgeRetrievalDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.IfElse]: {
|
||||
type: BlockEnum.IfElse,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IfElseDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Iteration]: {
|
||||
type: BlockEnum.Iteration,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IterationDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.IterationStart]: {
|
||||
type: BlockEnum.IterationStart,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IterationStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Loop]: {
|
||||
type: BlockEnum.Loop,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LoopStart]: {
|
||||
type: BlockEnum.LoopStart,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopEndDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
type: BlockEnum.Code,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
code_language: 'python3',
|
||||
code: '',
|
||||
outputs: [],
|
||||
...CodeDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.TemplateTransform]: {
|
||||
type: BlockEnum.TemplateTransform,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
template: '',
|
||||
...TemplateTransformDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.QuestionClassifier]: {
|
||||
type: BlockEnum.QuestionClassifier,
|
||||
title: '',
|
||||
desc: '',
|
||||
query_variable_selector: [],
|
||||
topics: [],
|
||||
...QuestionClassifierDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.HttpRequest]: {
|
||||
type: BlockEnum.HttpRequest,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
...HttpRequestDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.ParameterExtractor]: {
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
...ParameterExtractorDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.VariableAssigner]: {
|
||||
type: BlockEnum.VariableAssigner,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
output_type: '',
|
||||
...VariableAssignerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.VariableAggregator]: {
|
||||
type: BlockEnum.VariableAggregator,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
output_type: '',
|
||||
...VariableAssignerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Assigner]: {
|
||||
type: BlockEnum.Assigner,
|
||||
title: '',
|
||||
desc: '',
|
||||
...AssignerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Tool]: {
|
||||
type: BlockEnum.Tool,
|
||||
title: '',
|
||||
desc: '',
|
||||
...ToolDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.DocExtractor]: {
|
||||
type: BlockEnum.DocExtractor,
|
||||
title: '',
|
||||
desc: '',
|
||||
...DocExtractorDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.ListFilter]: {
|
||||
type: BlockEnum.ListFilter,
|
||||
title: '',
|
||||
desc: '',
|
||||
...ListFilterDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Agent]: {
|
||||
type: BlockEnum.Agent,
|
||||
title: '',
|
||||
desc: '',
|
||||
...AgentDefault.defaultValue,
|
||||
},
|
||||
}
|
||||
export const MAX_ITERATION_PARALLEL_NUM = 10
|
||||
export const MIN_ITERATION_PARALLEL_NUM = 1
|
||||
export const DEFAULT_ITER_TIMES = 1
|
||||
|
||||
42
web/app/components/workflow/constants/node.ts
Normal file
42
web/app/components/workflow/constants/node.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import llmDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import knowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
import agentDefault from '@/app/components/workflow/nodes/agent/default'
|
||||
|
||||
import questionClassifierDefault from '@/app/components/workflow/nodes/question-classifier/default'
|
||||
|
||||
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||
import iterationDefault from '@/app/components/workflow/nodes/iteration/default'
|
||||
import iterationStartDefault from '@/app/components/workflow/nodes/iteration-start/default'
|
||||
import loopDefault from '@/app/components/workflow/nodes/loop/default'
|
||||
import loopStartDefault from '@/app/components/workflow/nodes/loop-start/default'
|
||||
import loopEndDefault from '@/app/components/workflow/nodes/loop-end/default'
|
||||
|
||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
import templateTransformDefault from '@/app/components/workflow/nodes/template-transform/default'
|
||||
import variableAggregatorDefault from '@/app/components/workflow/nodes/variable-assigner/default'
|
||||
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
||||
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
|
||||
import parameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default'
|
||||
import listOperatorDefault from '@/app/components/workflow/nodes/list-operator/default'
|
||||
|
||||
export const WORKFLOW_COMMON_NODES = [
|
||||
llmDefault,
|
||||
knowledgeRetrievalDefault,
|
||||
agentDefault,
|
||||
questionClassifierDefault,
|
||||
ifElseDefault,
|
||||
iterationDefault,
|
||||
iterationStartDefault,
|
||||
loopDefault,
|
||||
loopStartDefault,
|
||||
loopEndDefault,
|
||||
codeDefault,
|
||||
templateTransformDefault,
|
||||
variableAggregatorDefault,
|
||||
documentExtractorDefault,
|
||||
assignerDefault,
|
||||
parameterExtractorDefault,
|
||||
httpRequestDefault,
|
||||
listOperatorDefault,
|
||||
]
|
||||
@ -56,8 +56,9 @@ const CustomEdge = ({
|
||||
})
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration, (data as Edge['data'])?.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration, (data as Edge['data'])?.isInLoop)
|
||||
const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration || (data as Edge['data'])?.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration || (data as Edge['data'])?.isInLoop)
|
||||
console.log(availableNextBlocks, 'xx')
|
||||
const {
|
||||
_sourceRunningStatus,
|
||||
_targetRunningStatus,
|
||||
|
||||
@ -7,8 +7,16 @@ import {
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { HooksStoreContext } from './provider'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type CommonHooksFnMap = {
|
||||
export type AvailableNodesMetaData = {
|
||||
nodes: NodeDefault[]
|
||||
nodesMap?: Record<BlockEnum, NodeDefault<any>>
|
||||
}
|
||||
export type CommonHooksFnMap = {
|
||||
doSyncWorkflowDraft: (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
@ -26,6 +34,7 @@ type CommonHooksFnMap = {
|
||||
handleStartWorkflowRun: () => void
|
||||
handleWorkflowStartRunInWorkflow: () => void
|
||||
handleWorkflowStartRunInChatflow: () => void
|
||||
availableNodesMetaData?: AvailableNodesMetaData
|
||||
}
|
||||
|
||||
export type Shape = {
|
||||
@ -43,6 +52,9 @@ export const createHooksStore = ({
|
||||
handleStartWorkflowRun = noop,
|
||||
handleWorkflowStartRunInWorkflow = noop,
|
||||
handleWorkflowStartRunInChatflow = noop,
|
||||
availableNodesMetaData = {
|
||||
nodes: [],
|
||||
},
|
||||
}: Partial<Shape>) => {
|
||||
return createStore<Shape>(set => ({
|
||||
refreshAll: props => set(state => ({ ...state, ...props })),
|
||||
@ -56,6 +68,7 @@ export const createHooksStore = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
availableNodesMetaData,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
export * from './use-edges-interactions'
|
||||
export * from './use-node-data-update'
|
||||
export * from './use-nodes-interactions'
|
||||
export * from './use-nodes-data'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
@ -16,3 +15,5 @@ export * from './use-shortcuts'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-format-time-from-now'
|
||||
export * from './use-nodes-meta-data'
|
||||
export * from './use-available-blocks'
|
||||
|
||||
58
web/app/components/workflow/hooks/use-available-blocks.ts
Normal file
58
web/app/components/workflow/hooks/use-available-blocks.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
|
||||
const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
|
||||
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!inContainer && nodeType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
const {
|
||||
nodes: availableNodes,
|
||||
} = useNodesMetaData()
|
||||
const availableNodesType = useMemo(() => availableNodes.map(node => node.type), [availableNodes])
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.Start)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
|
||||
const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
let availablePrevBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.Start)
|
||||
availablePrevBlocks = []
|
||||
|
||||
let availableNextBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd)
|
||||
availableNextBlocks = []
|
||||
|
||||
return {
|
||||
availablePrevBlocks: availablePrevBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
availableNextBlocks: availableNextBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
}
|
||||
}, [availableNodesType])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
getAvailableBlocks,
|
||||
availablePrevBlocks: availablePrevBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
availableNextBlocks: availableNextBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
}
|
||||
}, [getAvailableBlocks, availablePrevBlocks, availableNextBlocks, inContainer])
|
||||
}
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from '../constants'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
@ -37,7 +37,7 @@ import { fetchDatasets } from '@/service/datasets'
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const isChatMode = useIsChatMode()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
@ -100,7 +100,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
const { errorMessage } = nodesExtraData[node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
const { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
|
||||
list.push({
|
||||
@ -148,7 +148,7 @@ export const useChecklistBeforePublish = () => {
|
||||
const { notify } = useToastContext()
|
||||
const isChatMode = useIsChatMode()
|
||||
const store = useStoreApi()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
|
||||
const updateTime = useRef(0)
|
||||
@ -228,7 +228,7 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
|
||||
const checkData = getCheckData(node.data, datasets)
|
||||
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
|
||||
const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` })
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { BlockEnum } from '../types'
|
||||
import {
|
||||
NODES_EXTRA_DATA,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../constants'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
|
||||
export const useNodesInitialData = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(() => produce(NODES_INITIAL_DATA, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key as BlockEnum].title = t(`workflow.blocks.${key}`)
|
||||
})
|
||||
}), [t])
|
||||
}
|
||||
|
||||
export const useNodesExtraData = () => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
return useMemo(() => produce(NODES_EXTRA_DATA, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key as BlockEnum].about = t(`workflow.blocksAbout.${key}`)
|
||||
draft[key as BlockEnum].availablePrevNodes = draft[key as BlockEnum].getAvailablePrevNodes(isChatMode)
|
||||
draft[key as BlockEnum].availableNextNodes = draft[key as BlockEnum].getAvailableNextNodes(isChatMode)
|
||||
})
|
||||
}), [t, isChatMode])
|
||||
}
|
||||
|
||||
export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean, isInLoop?: boolean) => {
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType)
|
||||
return []
|
||||
return nodesExtraData[nodeType].availablePrevNodes || []
|
||||
}, [nodeType, nodesExtraData])
|
||||
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType)
|
||||
return []
|
||||
|
||||
return nodesExtraData[nodeType].availableNextNodes || []
|
||||
}, [nodeType, nodesExtraData])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
availablePrevBlocks: availablePrevBlocks.filter((nType) => {
|
||||
if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}),
|
||||
availableNextBlocks: availableNextBlocks.filter((nType) => {
|
||||
if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}),
|
||||
}
|
||||
}, [isInIteration, availablePrevBlocks, availableNextBlocks, isInLoop])
|
||||
}
|
||||
@ -31,7 +31,6 @@ import {
|
||||
ITERATION_PADDING,
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
LOOP_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
X_OFFSET,
|
||||
Y_OFFSET,
|
||||
@ -60,6 +59,7 @@ import {
|
||||
useWorkflowReadOnly,
|
||||
} from './use-workflow'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
|
||||
export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -84,6 +84,7 @@ export const useNodesInteractions = () => {
|
||||
handleNodeLoopChildrenCopy,
|
||||
} = useNodeLoopInteractions()
|
||||
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const { saveStateToHistory, undo, redo } = useWorkflowHistory()
|
||||
|
||||
@ -682,6 +683,10 @@ export const useNodesInteractions = () => {
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
const {
|
||||
defaultValue,
|
||||
title,
|
||||
} = nodesMetaDataMap![nodeType]
|
||||
const {
|
||||
newNode,
|
||||
newIterationStartNode,
|
||||
@ -689,8 +694,8 @@ export const useNodesInteractions = () => {
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
...(defaultValue as any),
|
||||
title: nodesWithSameType.length > 0 ? `${title} ${nodesWithSameType.length + 1}` : title,
|
||||
...(toolDefaultValue || {}),
|
||||
selected: true,
|
||||
_showAddVariablePopup: (nodeType === BlockEnum.VariableAssigner || nodeType === BlockEnum.VariableAggregator) && !!prevNodeId,
|
||||
@ -1093,7 +1098,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
|
||||
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit])
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit, nodesMetaDataMap])
|
||||
|
||||
const handleNodeChange = useCallback((
|
||||
currentNodeId: string,
|
||||
@ -1114,6 +1119,10 @@ export const useNodesInteractions = () => {
|
||||
const currentNode = nodes.find(node => node.id === currentNodeId)!
|
||||
const connectedEdges = getConnectedEdges([currentNode], edges)
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
const {
|
||||
defaultValue,
|
||||
title,
|
||||
} = nodesMetaDataMap![nodeType]
|
||||
const {
|
||||
newNode: newCurrentNode,
|
||||
newIterationStartNode,
|
||||
@ -1121,8 +1130,8 @@ export const useNodesInteractions = () => {
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
...(defaultValue as any),
|
||||
title: nodesWithSameType.length > 0 ? `${title} ${nodesWithSameType.length + 1}` : title,
|
||||
...(toolDefaultValue || {}),
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
@ -1175,7 +1184,7 @@ export const useNodesInteractions = () => {
|
||||
handleSyncWorkflowDraft()
|
||||
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeChange)
|
||||
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, nodesMetaDataMap])
|
||||
|
||||
const handleNodesCancelSelected = useCallback(() => {
|
||||
const {
|
||||
@ -1285,7 +1294,7 @@ export const useNodesInteractions = () => {
|
||||
} = generateNewNode({
|
||||
type: nodeToPaste.type,
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodesMetaDataMap![nodeType].defaultValue,
|
||||
...nodeToPaste.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
@ -1361,7 +1370,7 @@ export const useNodesInteractions = () => {
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodePaste)
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, [getNodesReadOnly, workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy, handleNodeLoopChildrenCopy])
|
||||
}, [getNodesReadOnly, workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy, handleNodeLoopChildrenCopy, nodesMetaDataMap])
|
||||
|
||||
const handleNodesDuplicate = useCallback((nodeId?: string) => {
|
||||
if (getNodesReadOnly())
|
||||
|
||||
14
web/app/components/workflow/hooks/use-nodes-meta-data.ts
Normal file
14
web/app/components/workflow/hooks/use-nodes-meta-data.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useNodesMetaData = () => {
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
nodes: availableNodesMetaData?.nodes || [],
|
||||
nodesMap: availableNodesMetaData?.nodesMap || {},
|
||||
} as AvailableNodesMetaData
|
||||
}, [availableNodesMetaData])
|
||||
}
|
||||
@ -32,7 +32,7 @@ import {
|
||||
} from '../constants'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useAvailableBlocks } from './use-available-blocks'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
@ -55,7 +55,7 @@ export const useWorkflow = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { getAvailableBlocks } = useAvailableBlocks()
|
||||
const setPanelWidth = useCallback((width: number) => {
|
||||
localStorage.setItem('workflow-node-panel-width', `${width}`)
|
||||
workflowStore.setState({ panelWidth: width })
|
||||
@ -361,8 +361,8 @@ export const useWorkflow = () => {
|
||||
return false
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
|
||||
const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
|
||||
const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks
|
||||
const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks
|
||||
|
||||
if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
|
||||
return false
|
||||
@ -386,7 +386,7 @@ export const useWorkflow = () => {
|
||||
}
|
||||
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, nodesExtraData, checkParallelLimit])
|
||||
}, [store, checkParallelLimit, getAvailableBlocks])
|
||||
|
||||
const getNode = useCallback((nodeId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
|
||||
@ -17,7 +17,7 @@ const ErrorHandleTip = ({
|
||||
|
||||
if (type === ErrorHandleTypeEnum.defaultValue)
|
||||
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
|
||||
}, [])
|
||||
}, [t, type])
|
||||
|
||||
if (!type)
|
||||
return null
|
||||
|
||||
@ -38,7 +38,7 @@ const Add = ({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration, nodeData.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
const { checkParallelLimit } = useWorkflow()
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
|
||||
@ -36,7 +36,7 @@ const ChangeItem = ({
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
|
||||
@ -47,7 +47,7 @@ export const NodeTargetHandle = memo(({
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availablePrevBlocks.length
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
@ -129,7 +129,7 @@ export const NodeSourceHandle = memo(({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availableNextBlocks.length
|
||||
const isChatMode = useIsChatMode()
|
||||
const { checkParallelLimit } = useWorkflow()
|
||||
|
||||
@ -30,7 +30,7 @@ const ChangeBlock = ({
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration, nodeData.isInLoop)
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
if (availablePrevBlocks.length && availableNextBlocks.length)
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesMetaData,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
@ -48,14 +48,14 @@ const PanelOperatorPopup = ({
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const author = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].author
|
||||
return nodesExtraData![data.type].author
|
||||
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.author
|
||||
@ -68,7 +68,7 @@ const PanelOperatorPopup = ({
|
||||
|
||||
const about = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].about
|
||||
return nodesExtraData![data.type].description
|
||||
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.description[language]
|
||||
|
||||
@ -772,6 +772,9 @@ export const getVarType = ({
|
||||
|
||||
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
|
||||
if (isStructuredOutputVar) {
|
||||
if (valueSelector.length === 2) { // root
|
||||
return VarType.object
|
||||
}
|
||||
let currProperties = targetVar.children.schema;
|
||||
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 3
|
||||
|
||||
@ -258,6 +258,8 @@ type Props = {
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
itemWidth?: number
|
||||
maxHeightClass?: string
|
||||
onClose?: () => void
|
||||
onBlur?: () => void
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
hideSearch,
|
||||
@ -267,10 +269,19 @@ const VarReferenceVars: FC<Props> = ({
|
||||
onChange,
|
||||
itemWidth,
|
||||
maxHeightClass,
|
||||
onClose,
|
||||
onBlur,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
const filteredVars = vars.filter((v) => {
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
|
||||
return children.length > 0
|
||||
@ -301,14 +312,17 @@ const VarReferenceVars: FC<Props> = ({
|
||||
{
|
||||
!hideSearch && (
|
||||
<>
|
||||
<div className={cn('mx-2 mb-1 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<div className={cn('var-search-input-wrapper mx-2 mb-1 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<Input
|
||||
className='var-search-input'
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={searchText}
|
||||
placeholder={t('workflow.common.searchVar') || ''}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClear={() => setSearchText('')}
|
||||
onBlur={onBlur}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,69 +1,15 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
const language = useGetLanguage()
|
||||
const prefixLink = useMemo(() => {
|
||||
if (language === 'zh_Hans')
|
||||
return 'https://docs.dify.ai/zh-hans/guides/workflow/node/'
|
||||
const availableNodesMetaData = useNodesMetaData()
|
||||
|
||||
return 'https://docs.dify.ai/guides/workflow/node/'
|
||||
}, [language])
|
||||
const linkMap = useMemo(() => {
|
||||
if (language === 'zh_Hans') {
|
||||
return {
|
||||
[BlockEnum.Start]: 'start',
|
||||
[BlockEnum.End]: 'end',
|
||||
[BlockEnum.Answer]: 'answer',
|
||||
[BlockEnum.LLM]: 'llm',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question-classifier',
|
||||
[BlockEnum.IfElse]: 'ifelse',
|
||||
[BlockEnum.Code]: 'code',
|
||||
[BlockEnum.TemplateTransform]: 'template',
|
||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
[BlockEnum.DocExtractor]: 'doc-extractor',
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}
|
||||
const link = useMemo(() => {
|
||||
const result = availableNodesMetaData?.nodesMap?.[nodeType]?.helpLinkUri || ''
|
||||
|
||||
return {
|
||||
[BlockEnum.Start]: 'start',
|
||||
[BlockEnum.End]: 'end',
|
||||
[BlockEnum.Answer]: 'answer',
|
||||
[BlockEnum.LLM]: 'llm',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question-classifier',
|
||||
[BlockEnum.IfElse]: 'ifelse',
|
||||
[BlockEnum.Code]: 'code',
|
||||
[BlockEnum.TemplateTransform]: 'template',
|
||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
[BlockEnum.DocExtractor]: 'doc-extractor',
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}, [language]) as Record<string, string>
|
||||
return result
|
||||
}, [availableNodesMetaData, nodeType])
|
||||
|
||||
const link = linkMap[nodeType]
|
||||
|
||||
if (!link)
|
||||
return ''
|
||||
|
||||
return `${prefixLink}${link}`
|
||||
return link
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const handleResize = useCallback((width: number) => {
|
||||
|
||||
@ -1,23 +1,18 @@
|
||||
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { AgentNodeType } from './types'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { renderI18nObject } from '@/i18n'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const nodeDefault: NodeDefault<AgentNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: 3,
|
||||
type: BlockEnum.Agent,
|
||||
}),
|
||||
defaultValue: {
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode) {
|
||||
return isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
},
|
||||
getAvailableNextNodes(isChatMode) {
|
||||
return isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
},
|
||||
checkValid(payload, t, moreDataForCheckValid: {
|
||||
strategyProvider?: StrategyPluginDetail,
|
||||
strategy?: StrategyDetail
|
||||
|
||||
@ -1,25 +1,17 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { AnswerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const nodeDefault: NodeDefault<AnswerNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: 2.1,
|
||||
type: BlockEnum.Answer,
|
||||
}),
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
answer: '',
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: AnswerNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { answer } = payload
|
||||
|
||||
@ -1,24 +1,21 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { type AssignerNodeType, WriteMode } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<AssignerNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
sort: 5,
|
||||
type: BlockEnum.Assigner,
|
||||
helpLinkUri: 'variable-assigner',
|
||||
}),
|
||||
defaultValue: {
|
||||
version: '2',
|
||||
items: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: AssignerNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const {
|
||||
|
||||
@ -1,27 +1,23 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { CodeLanguage, type CodeNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<CodeNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
sort: 1,
|
||||
type: BlockEnum.Code,
|
||||
}),
|
||||
defaultValue: {
|
||||
code: '',
|
||||
code_language: CodeLanguage.python3,
|
||||
variables: [],
|
||||
outputs: {},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: CodeNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { code, variables } = payload
|
||||
|
||||
@ -1,24 +1,21 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { DocExtractorNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<DocExtractorNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
sort: 4,
|
||||
type: BlockEnum.DocExtractor,
|
||||
helpLinkUri: 'doc-extractor',
|
||||
}),
|
||||
defaultValue: {
|
||||
variable_selector: [],
|
||||
is_array_file: false,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: DocExtractorNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variable_selector: variable } = payload
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { EndNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const nodeDefault: NodeDefault<EndNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: 2.1,
|
||||
type: BlockEnum.End,
|
||||
}),
|
||||
defaultValue: {
|
||||
outputs: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes() {
|
||||
return []
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { AuthorizationType, BodyType, Method } from './types'
|
||||
import type { BodyPayload, HttpNodeType } from './types'
|
||||
import {
|
||||
ALL_CHAT_AVAILABLE_BLOCKS,
|
||||
ALL_COMPLETION_AVAILABLE_BLOCKS,
|
||||
} from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
sort: 1,
|
||||
type: BlockEnum.HttpRequest,
|
||||
}),
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
method: Method.get,
|
||||
@ -33,16 +36,6 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
retry_interval: 100,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: HttpNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
import { BlockEnum, type NodeDefault } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { type IfElseNodeType, LogicalOperator } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<IfElseNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
sort: 1,
|
||||
type: BlockEnum.IfElse,
|
||||
helpLinkUri: 'ifelse',
|
||||
}),
|
||||
defaultValue: {
|
||||
_targetBranches: [
|
||||
{
|
||||
@ -24,16 +32,6 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: IfElseNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { cases } = payload
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { IterationStartNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const nodeDefault: NodeDefault<IterationStartNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: -1,
|
||||
type: BlockEnum.IterationStart,
|
||||
}),
|
||||
defaultValue: {},
|
||||
getAvailablePrevNodes() {
|
||||
return []
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { BlockEnum, ErrorHandleMode } from '../../types'
|
||||
import { ErrorHandleMode } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { IterationNodeType } from './types'
|
||||
import {
|
||||
ALL_CHAT_AVAILABLE_BLOCKS,
|
||||
ALL_COMPLETION_AVAILABLE_BLOCKS,
|
||||
} from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<IterationNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
sort: 2,
|
||||
type: BlockEnum.Iteration,
|
||||
}),
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
iterator_selector: [],
|
||||
@ -18,20 +22,6 @@ const nodeDefault: NodeDefault<IterationNodeType> = {
|
||||
parallel_nums: 10,
|
||||
error_handle_mode: ErrorHandleMode.Terminated,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(
|
||||
type => type !== BlockEnum.End,
|
||||
)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: IterationNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
|
||||
@ -12,13 +12,14 @@ import {
|
||||
} from '../../utils'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../iteration-start/constants'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useNodeIterationInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const handleNodeIterationRerender = useCallback((nodeId: string) => {
|
||||
const {
|
||||
@ -120,7 +121,7 @@ export const useNodeIterationInteractions = () => {
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...nodesMetaDataMap![childNodeType].defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { KnowledgeRetrievalNodeType } from './types'
|
||||
import { checkoutRerankModelConfigedInRetrievalSettings } from './utils'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { DATASET_DEFAULT } from '@/config'
|
||||
import { RETRIEVE_TYPE } from '@/types/app'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<KnowledgeRetrievalNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: 2,
|
||||
type: BlockEnum.KnowledgeRetrieval,
|
||||
}),
|
||||
defaultValue: {
|
||||
query_variable_selector: [],
|
||||
dataset_ids: [],
|
||||
@ -18,16 +22,6 @@ const nodeDefault: NodeDefault<KnowledgeRetrievalNodeType> = {
|
||||
reranking_enable: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: KnowledgeRetrievalNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && (!payload.query_variable_selector || payload.query_variable_selector.length === 0))
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { comparisonOperatorNotRequireValue } from '../if-else/utils'
|
||||
import { type ListFilterNodeType, OrderBy } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
sort: 2,
|
||||
type: BlockEnum.ListFilter,
|
||||
}),
|
||||
defaultValue: {
|
||||
variable: [],
|
||||
filter_by: {
|
||||
@ -26,16 +33,6 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: ListFilterNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variable, var_type, filter_by } = payload
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { BlockEnum, EditionType } from '../../types'
|
||||
import { EditionType } from '../../types'
|
||||
import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
|
||||
import type { LLMNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<LLMNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: 1,
|
||||
type: BlockEnum.LLM,
|
||||
}),
|
||||
defaultValue: {
|
||||
model: {
|
||||
provider: '',
|
||||
@ -27,16 +32,6 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: LLMNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && !payload.model.provider)
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import type {
|
||||
SimpleNodeType,
|
||||
} from '@/app/components/workflow/simple-node/types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
const nodeDefault: NodeDefault<SimpleNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
sort: 2,
|
||||
type: BlockEnum.LoopEnd,
|
||||
}),
|
||||
defaultValue: {},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes() {
|
||||
return []
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { LoopStartNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const nodeDefault: NodeDefault<LoopStartNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: -1,
|
||||
type: BlockEnum.LoopStart,
|
||||
}),
|
||||
defaultValue: {},
|
||||
getAvailablePrevNodes() {
|
||||
return []
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
|
||||
@ -32,7 +32,7 @@ const AddBlock = ({
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false, true)
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, true)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { ComparisonOperator, LogicalOperator, type LoopNodeType } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { LOOP_NODE_MAX_COUNT } from '@/config'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<LoopNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
sort: 3,
|
||||
type: BlockEnum.Loop,
|
||||
// author: 'AICT-Team',
|
||||
}),
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
break_conditions: [],
|
||||
@ -15,16 +22,6 @@ const nodeDefault: NodeDefault<LoopNodeType> = {
|
||||
_children: [],
|
||||
logical_operator: LogicalOperator.and,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: LoopNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
@ -12,13 +11,13 @@ import {
|
||||
} from '../../utils'
|
||||
import {
|
||||
LOOP_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../loop-start/constants'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useNodeLoopInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const handleNodeLoopRerender = useCallback((nodeId: string) => {
|
||||
const {
|
||||
@ -115,17 +114,21 @@ export const useNodeLoopInteractions = () => {
|
||||
|
||||
return childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const {
|
||||
defaultValue,
|
||||
title,
|
||||
} = nodesMetaDataMap![childNodeType]
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${childNodeType}`),
|
||||
title: nodesWithSameType.length > 0 ? `${title} ${nodesWithSameType.length + 1}` : title,
|
||||
loop_id: newNodeId,
|
||||
|
||||
},
|
||||
@ -138,7 +141,7 @@ export const useNodeLoopInteractions = () => {
|
||||
newNode.id = `${newNodeId}${newNode.id + index}`
|
||||
return newNode
|
||||
})
|
||||
}, [store, t])
|
||||
}, [store, nodesMetaDataMap])
|
||||
|
||||
return {
|
||||
handleNodeLoopRerender,
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { type ParameterExtractorNodeType, ReasoningModeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
sort: 6,
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
}),
|
||||
defaultValue: {
|
||||
query: [],
|
||||
model: {
|
||||
@ -20,16 +26,6 @@ const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: ParameterExtractorNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && (!payload.query || payload.query.length === 0))
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { QuestionClassifierNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.QuestionUnderstand,
|
||||
sort: 1,
|
||||
type: BlockEnum.QuestionClassifier,
|
||||
}),
|
||||
defaultValue: {
|
||||
query_variable_selector: [],
|
||||
model: {
|
||||
@ -40,16 +46,6 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: QuestionClassifierNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && (!payload.query_variable_selector || payload.query_variable_selector.length === 0))
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { StartNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const nodeDefault: NodeDefault<StartNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: 0.1,
|
||||
type: BlockEnum.Start,
|
||||
}),
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
},
|
||||
getAvailablePrevNodes() {
|
||||
return []
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { TemplateTransformNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<TemplateTransformNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
sort: 2,
|
||||
type: BlockEnum.TemplateTransform,
|
||||
helpLinkUri: 'template',
|
||||
}),
|
||||
defaultValue: {
|
||||
template: '',
|
||||
variables: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: TemplateTransformNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { template, variables } = payload
|
||||
|
||||
@ -1,26 +1,21 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { ToolNodeType } from './types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<ToolNodeType> = {
|
||||
...genNodeMetaData({
|
||||
sort: -1,
|
||||
type: BlockEnum.Tool,
|
||||
helpLinkUri: 'tools',
|
||||
}),
|
||||
defaultValue: {
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: ToolNodeType, t: any, moreDataForCheckValid: any) {
|
||||
const { toolInputsSchema, toolSettingSchema, language, notAuthed } = moreDataForCheckValid
|
||||
let errorMessages = ''
|
||||
|
||||
@ -1,25 +1,21 @@
|
||||
import { type NodeDefault, VarType } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
|
||||
...genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Transform,
|
||||
sort: 3,
|
||||
type: BlockEnum.VariableAggregator,
|
||||
}),
|
||||
defaultValue: {
|
||||
output_type: VarType.any,
|
||||
variables: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: VariableAssignerNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variables, advanced_settings } = payload
|
||||
|
||||
@ -13,10 +13,10 @@ import {
|
||||
} from '../utils'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesMetaData,
|
||||
useNodesReadOnly,
|
||||
usePanelInteractions,
|
||||
} from '../hooks'
|
||||
import { NODES_INITIAL_DATA } from '../constants'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import TipPopup from './tip-popup'
|
||||
import cn from '@/utils/classnames'
|
||||
@ -43,6 +43,7 @@ const AddBlock = ({
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open)
|
||||
@ -56,11 +57,15 @@ const AddBlock = ({
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const {
|
||||
defaultValue,
|
||||
title,
|
||||
} = nodesMetaDataMap![type]
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(type),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
...(defaultValue as any),
|
||||
title: nodesWithSameType.length > 0 ? `${title} ${nodesWithSameType.length + 1}` : title,
|
||||
...(toolDefaultValue || {}),
|
||||
_isCandidate: true,
|
||||
},
|
||||
@ -72,7 +77,7 @@ const AddBlock = ({
|
||||
workflowStore.setState({
|
||||
candidateNode: newNode,
|
||||
})
|
||||
}, [store, workflowStore, t])
|
||||
}, [store, workflowStore, nodesMetaDataMap])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
@ -47,10 +48,45 @@ const WorkflowPreview = () => {
|
||||
switchTab('DETAIL')
|
||||
}, [workflowRunningData])
|
||||
|
||||
const [panelWidth, setPanelWidth] = useState(420)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
|
||||
const startResizing = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setIsResizing(true)
|
||||
}, [])
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setIsResizing(false)
|
||||
}, [])
|
||||
|
||||
const resize = useCallback((e: MouseEvent) => {
|
||||
if (isResizing) {
|
||||
const newWidth = window.innerWidth - e.clientX
|
||||
if (newWidth > 420 && newWidth < 1024)
|
||||
setPanelWidth(newWidth)
|
||||
}
|
||||
}, [isResizing])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', resize)
|
||||
window.addEventListener('mouseup', stopResizing)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', resize)
|
||||
window.removeEventListener('mouseup', stopResizing)
|
||||
}
|
||||
}, [resize, stopResizing])
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
flex h-full w-[420px] flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl
|
||||
`}>
|
||||
relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl
|
||||
`}
|
||||
style={{ width: `${panelWidth}px` }}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 left-[3px] top-1/2 z-50 h-6 w-[3px] cursor-col-resize rounded bg-gray-300"
|
||||
onMouseDown={startResizing}
|
||||
/>
|
||||
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'>
|
||||
{`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`}
|
||||
<div className='cursor-pointer p-1' onClick={() => handleCancelDebugAndPreviewPanel()}>
|
||||
|
||||
@ -15,6 +15,7 @@ import type {
|
||||
} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
|
||||
import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
export enum BlockEnum {
|
||||
Start = 'start',
|
||||
@ -289,17 +290,15 @@ export type NodeOutPutVar = {
|
||||
isLoop?: boolean
|
||||
}
|
||||
|
||||
export type Block = {
|
||||
classification?: string
|
||||
export type NodeDefault<T = {}> = {
|
||||
classification: BlockClassificationEnum
|
||||
sort: number
|
||||
type: BlockEnum
|
||||
title: string
|
||||
author: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type NodeDefault<T> = {
|
||||
helpLinkUri?: string
|
||||
defaultValue: Partial<T>
|
||||
getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[]
|
||||
getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[]
|
||||
checkValid: (payload: T, t: any, moreDataForCheckValid?: any) => { isValid: boolean; errorMessage?: string }
|
||||
}
|
||||
|
||||
|
||||
28
web/app/components/workflow/utils/gen-node-meta-data.ts
Normal file
28
web/app/components/workflow/utils/gen-node-meta-data.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export type GenNodeMetaDataParams = {
|
||||
classification?: BlockClassificationEnum
|
||||
sort: number
|
||||
type: BlockEnum
|
||||
title?: string
|
||||
author?: string
|
||||
helpLinkUri?: string
|
||||
}
|
||||
export const genNodeMetaData = ({
|
||||
classification = BlockClassificationEnum.Default,
|
||||
sort,
|
||||
type,
|
||||
title = '',
|
||||
author = 'Dify',
|
||||
helpLinkUri,
|
||||
}: GenNodeMetaDataParams) => {
|
||||
return {
|
||||
classification,
|
||||
sort,
|
||||
type,
|
||||
title,
|
||||
author,
|
||||
helpLinkUri: helpLinkUri || type,
|
||||
}
|
||||
}
|
||||
@ -6,3 +6,4 @@ export * from './common'
|
||||
export * from './tool'
|
||||
export * from './workflow'
|
||||
export * from './variable'
|
||||
export * from './gen-node-meta-data'
|
||||
|
||||
Reference in New Issue
Block a user