mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 16:37:15 +08:00
feat: v1
This commit is contained in:
parent
446df6b50d
commit
a5c6c8638e
@ -55,6 +55,14 @@ class InstructionTemplatePayload(BaseModel):
|
|||||||
type: str = Field(..., description="Instruction template type")
|
type: str = Field(..., description="Instruction template type")
|
||||||
|
|
||||||
|
|
||||||
|
class FlowchartGeneratePayload(BaseModel):
|
||||||
|
instruction: str = Field(..., description="Workflow flowchart generation instruction")
|
||||||
|
model_config_data: dict[str, Any] = Field(..., alias="model_config", description="Model configuration")
|
||||||
|
available_nodes: list[dict[str, Any]] = Field(default_factory=list, description="Available node types")
|
||||||
|
existing_nodes: list[dict[str, Any]] = Field(default_factory=list, description="Existing workflow nodes")
|
||||||
|
available_tools: list[dict[str, Any]] = Field(default_factory=list, description="Available tools")
|
||||||
|
|
||||||
|
|
||||||
def reg(cls: type[BaseModel]):
|
def reg(cls: type[BaseModel]):
|
||||||
console_ns.schema_model(cls.__name__, cls.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0))
|
console_ns.schema_model(cls.__name__, cls.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0))
|
||||||
|
|
||||||
@ -64,6 +72,7 @@ reg(RuleCodeGeneratePayload)
|
|||||||
reg(RuleStructuredOutputPayload)
|
reg(RuleStructuredOutputPayload)
|
||||||
reg(InstructionGeneratePayload)
|
reg(InstructionGeneratePayload)
|
||||||
reg(InstructionTemplatePayload)
|
reg(InstructionTemplatePayload)
|
||||||
|
reg(FlowchartGeneratePayload)
|
||||||
|
|
||||||
|
|
||||||
@console_ns.route("/rule-generate")
|
@console_ns.route("/rule-generate")
|
||||||
@ -255,6 +264,42 @@ class InstructionGenerateApi(Resource):
|
|||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/flowchart-generate")
|
||||||
|
class FlowchartGenerateApi(Resource):
|
||||||
|
@console_ns.doc("generate_workflow_flowchart")
|
||||||
|
@console_ns.doc(description="Generate workflow flowchart using LLM")
|
||||||
|
@console_ns.expect(console_ns.models[FlowchartGeneratePayload.__name__])
|
||||||
|
@console_ns.response(200, "Flowchart generated successfully")
|
||||||
|
@console_ns.response(400, "Invalid request parameters")
|
||||||
|
@console_ns.response(402, "Provider quota exceeded")
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
args = FlowchartGeneratePayload.model_validate(console_ns.payload)
|
||||||
|
_, current_tenant_id = current_account_with_tenant()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = LLMGenerator.generate_workflow_flowchart(
|
||||||
|
tenant_id=current_tenant_id,
|
||||||
|
instruction=args.instruction,
|
||||||
|
model_config=args.model_config_data,
|
||||||
|
available_nodes=args.available_nodes,
|
||||||
|
existing_nodes=args.existing_nodes,
|
||||||
|
available_tools=args.available_tools,
|
||||||
|
)
|
||||||
|
except ProviderTokenNotInitError as ex:
|
||||||
|
raise ProviderNotInitializeError(ex.description)
|
||||||
|
except QuotaExceededError:
|
||||||
|
raise ProviderQuotaExceededError()
|
||||||
|
except ModelCurrentlyNotSupportError:
|
||||||
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
|
except InvokeError as e:
|
||||||
|
raise CompletionRequestError(e.description)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@console_ns.route("/instruction-generate/template")
|
@console_ns.route("/instruction-generate/template")
|
||||||
class InstructionGenerationTemplateApi(Resource):
|
class InstructionGenerationTemplateApi(Resource):
|
||||||
@console_ns.doc("get_instruction_template")
|
@console_ns.doc("get_instruction_template")
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from core.llm_generator.prompts import (
|
|||||||
SUGGESTED_QUESTIONS_MAX_TOKENS,
|
SUGGESTED_QUESTIONS_MAX_TOKENS,
|
||||||
SUGGESTED_QUESTIONS_TEMPERATURE,
|
SUGGESTED_QUESTIONS_TEMPERATURE,
|
||||||
SYSTEM_STRUCTURED_OUTPUT_GENERATE,
|
SYSTEM_STRUCTURED_OUTPUT_GENERATE,
|
||||||
|
WORKFLOW_FLOWCHART_PROMPT_TEMPLATE,
|
||||||
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
|
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
|
||||||
)
|
)
|
||||||
from core.model_manager import ModelManager
|
from core.model_manager import ModelManager
|
||||||
@ -285,6 +286,61 @@ class LLMGenerator:
|
|||||||
|
|
||||||
return rule_config
|
return rule_config
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_workflow_flowchart(
|
||||||
|
cls,
|
||||||
|
tenant_id: str,
|
||||||
|
instruction: str,
|
||||||
|
model_config: dict,
|
||||||
|
available_nodes: Sequence[dict[str, object]] | None = None,
|
||||||
|
existing_nodes: Sequence[dict[str, object]] | None = None,
|
||||||
|
available_tools: Sequence[dict[str, object]] | None = None,
|
||||||
|
):
|
||||||
|
model_parameters = model_config.get("completion_params", {})
|
||||||
|
prompt_template = PromptTemplateParser(WORKFLOW_FLOWCHART_PROMPT_TEMPLATE)
|
||||||
|
prompt_generate = prompt_template.format(
|
||||||
|
inputs={
|
||||||
|
"TASK_DESCRIPTION": instruction,
|
||||||
|
"AVAILABLE_NODES": json.dumps(available_nodes or [], ensure_ascii=False),
|
||||||
|
"EXISTING_NODES": json.dumps(existing_nodes or [], ensure_ascii=False),
|
||||||
|
"AVAILABLE_TOOLS": json.dumps(available_tools or [], ensure_ascii=False),
|
||||||
|
},
|
||||||
|
remove_template_variables=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt_messages = [UserPromptMessage(content=prompt_generate)]
|
||||||
|
|
||||||
|
model_manager = ModelManager()
|
||||||
|
model_instance = model_manager.get_model_instance(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
model_type=ModelType.LLM,
|
||||||
|
provider=model_config.get("provider", ""),
|
||||||
|
model=model_config.get("name", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
flowchart = ""
|
||||||
|
error = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response: LLMResult = model_instance.invoke_llm(
|
||||||
|
prompt_messages=list(prompt_messages),
|
||||||
|
model_parameters=model_parameters,
|
||||||
|
stream=False,
|
||||||
|
)
|
||||||
|
content = response.message.get_text_content()
|
||||||
|
if not isinstance(content, str):
|
||||||
|
raise ValueError("Flowchart response is not a string")
|
||||||
|
|
||||||
|
match = re.search(r"```(?:mermaid)?\s*([\s\S]+?)```", content, flags=re.IGNORECASE)
|
||||||
|
flowchart = (match.group(1) if match else content).strip()
|
||||||
|
except InvokeError as e:
|
||||||
|
error = str(e)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to generate workflow flowchart, model: %s", model_config.get("name"))
|
||||||
|
error = str(e)
|
||||||
|
|
||||||
|
return {"flowchart": flowchart, "error": error}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_code(cls, tenant_id: str, instruction: str, model_config: dict, code_language: str = "javascript"):
|
def generate_code(cls, tenant_id: str, instruction: str, model_config: dict, code_language: str = "javascript"):
|
||||||
if code_language == "python":
|
if code_language == "python":
|
||||||
|
|||||||
@ -143,6 +143,40 @@ Based on task description, please create a well-structured prompt template that
|
|||||||
Please generate the full prompt template with at least 300 words and output only the prompt template.
|
Please generate the full prompt template with at least 300 words and output only the prompt template.
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
|
|
||||||
|
WORKFLOW_FLOWCHART_PROMPT_TEMPLATE = """
|
||||||
|
You are an expert workflow designer. Generate a Mermaid flowchart based on the user's request.
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
- Use only node types listed in <available_nodes>.
|
||||||
|
- Use only tools listed in <available_tools>. When using a tool node, set type=tool and tool=<provider_id>/<tool_name>.
|
||||||
|
- Prefer reusing node titles from <existing_nodes> when possible.
|
||||||
|
- Output must be valid Mermaid flowchart syntax, no markdown, no extra text.
|
||||||
|
- First line must be: flowchart LR
|
||||||
|
- Every node must be declared on its own line using:
|
||||||
|
<id>["type=<type>|title=<title>|tool=<provider_id>/<tool_name>"]
|
||||||
|
- type is required and must match a type in <available_nodes>.
|
||||||
|
- title is required for non-tool nodes.
|
||||||
|
- tool is required only when type=tool, otherwise omit tool.
|
||||||
|
- Edges must use:
|
||||||
|
<id> --> <id>
|
||||||
|
<id> -->|true| <id>
|
||||||
|
<id> -->|false| <id>
|
||||||
|
- Keep node ids unique and simple (N1, N2, ...).
|
||||||
|
|
||||||
|
<user_request>
|
||||||
|
{{TASK_DESCRIPTION}}
|
||||||
|
</user_request>
|
||||||
|
<available_nodes>
|
||||||
|
{{AVAILABLE_NODES}}
|
||||||
|
</available_nodes>
|
||||||
|
<existing_nodes>
|
||||||
|
{{EXISTING_NODES}}
|
||||||
|
</existing_nodes>
|
||||||
|
<available_tools>
|
||||||
|
{{AVAILABLE_TOOLS}}
|
||||||
|
</available_tools>
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
RULE_CONFIG_PROMPT_GENERATE_TEMPLATE = """
|
RULE_CONFIG_PROMPT_GENERATE_TEMPLATE = """
|
||||||
Here is a task description for which I would like you to create a high-quality prompt template for:
|
Here is a task description for which I would like you to create a high-quality prompt template for:
|
||||||
<task_description>
|
<task_description>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { forumCommand } from './forum'
|
|||||||
import { languageCommand } from './language'
|
import { languageCommand } from './language'
|
||||||
import { slashCommandRegistry } from './registry'
|
import { slashCommandRegistry } from './registry'
|
||||||
import { themeCommand } from './theme'
|
import { themeCommand } from './theme'
|
||||||
|
import { vibeCommand } from './vibe'
|
||||||
import { zenCommand } from './zen'
|
import { zenCommand } from './zen'
|
||||||
|
|
||||||
export const slashAction: ActionItem = {
|
export const slashAction: ActionItem = {
|
||||||
@ -41,6 +42,7 @@ export const registerSlashCommands = (deps: Record<string, any>) => {
|
|||||||
slashCommandRegistry.register(communityCommand, {})
|
slashCommandRegistry.register(communityCommand, {})
|
||||||
slashCommandRegistry.register(accountCommand, {})
|
slashCommandRegistry.register(accountCommand, {})
|
||||||
slashCommandRegistry.register(zenCommand, {})
|
slashCommandRegistry.register(zenCommand, {})
|
||||||
|
slashCommandRegistry.register(vibeCommand, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unregisterSlashCommands = () => {
|
export const unregisterSlashCommands = () => {
|
||||||
@ -52,6 +54,7 @@ export const unregisterSlashCommands = () => {
|
|||||||
slashCommandRegistry.unregister('community')
|
slashCommandRegistry.unregister('community')
|
||||||
slashCommandRegistry.unregister('account')
|
slashCommandRegistry.unregister('account')
|
||||||
slashCommandRegistry.unregister('zen')
|
slashCommandRegistry.unregister('zen')
|
||||||
|
slashCommandRegistry.unregister('vibe')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SlashCommandProvider = () => {
|
export const SlashCommandProvider = () => {
|
||||||
|
|||||||
59
web/app/components/goto-anything/actions/commands/vibe.tsx
Normal file
59
web/app/components/goto-anything/actions/commands/vibe.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import type { SlashCommandHandler } from './types'
|
||||||
|
import { RiSparklingFill } from '@remixicon/react'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { isInWorkflowPage, VIBE_COMMAND_EVENT } from '@/app/components/workflow/constants'
|
||||||
|
import i18n from '@/i18n-config/i18next-config'
|
||||||
|
import { registerCommands, unregisterCommands } from './command-bus'
|
||||||
|
|
||||||
|
type VibeDeps = Record<string, never>
|
||||||
|
|
||||||
|
const VIBE_PROMPT_EXAMPLE = 'Summarize a document, classify sentiment, then notify Slack'
|
||||||
|
|
||||||
|
const dispatchVibeCommand = (input?: string) => {
|
||||||
|
if (typeof document === 'undefined')
|
||||||
|
return
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent(VIBE_COMMAND_EVENT, { detail: { dsl: input } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vibeCommand: SlashCommandHandler<VibeDeps> = {
|
||||||
|
name: 'vibe',
|
||||||
|
description: i18n.t('app.gotoAnything.actions.vibeDesc'),
|
||||||
|
mode: 'submenu',
|
||||||
|
isAvailable: () => isInWorkflowPage(),
|
||||||
|
|
||||||
|
async search(args: string, locale: string = 'en') {
|
||||||
|
const trimmed = args.trim()
|
||||||
|
const hasInput = !!trimmed
|
||||||
|
|
||||||
|
return [{
|
||||||
|
id: 'vibe',
|
||||||
|
title: i18n.t('app.gotoAnything.actions.vibeTitle', { lng: locale }) || 'Vibe',
|
||||||
|
description: hasInput
|
||||||
|
? i18n.t('app.gotoAnything.actions.vibeDesc', { lng: locale })
|
||||||
|
: i18n.t('app.gotoAnything.actions.vibeHint', { lng: locale, prompt: VIBE_PROMPT_EXAMPLE }),
|
||||||
|
type: 'command' as const,
|
||||||
|
icon: (
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg">
|
||||||
|
<RiSparklingFill className="h-4 w-4 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
command: 'workflow.vibe',
|
||||||
|
args: { dsl: trimmed },
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
register(_deps: VibeDeps) {
|
||||||
|
registerCommands({
|
||||||
|
'workflow.vibe': async (args) => {
|
||||||
|
dispatchVibeCommand(args?.dsl)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
unregister() {
|
||||||
|
unregisterCommands(['workflow.vibe'])
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ export const NODE_WIDTH = 240
|
|||||||
export const X_OFFSET = 60
|
export const X_OFFSET = 60
|
||||||
export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET
|
export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET
|
||||||
export const Y_OFFSET = 39
|
export const Y_OFFSET = 39
|
||||||
|
export const VIBE_COMMAND_EVENT = 'workflow-vibe-command'
|
||||||
export const START_INITIAL_POSITION = { x: 80, y: 282 }
|
export const START_INITIAL_POSITION = { x: 80, y: 282 }
|
||||||
export const AUTO_LAYOUT_OFFSET = {
|
export const AUTO_LAYOUT_OFFSET = {
|
||||||
x: -42,
|
x: -42,
|
||||||
|
|||||||
@ -24,3 +24,4 @@ export * from './use-workflow-run'
|
|||||||
export * from './use-workflow-search'
|
export * from './use-workflow-search'
|
||||||
export * from './use-workflow-start-run'
|
export * from './use-workflow-start-run'
|
||||||
export * from './use-workflow-variables'
|
export * from './use-workflow-variables'
|
||||||
|
export * from './use-workflow-vibe'
|
||||||
|
|||||||
672
web/app/components/workflow/hooks/use-workflow-vibe.tsx
Normal file
672
web/app/components/workflow/hooks/use-workflow-vibe.tsx
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ToolDefaultValue } from '../block-selector/types'
|
||||||
|
import type { Edge, Node, ToolWithProvider } from '../types'
|
||||||
|
import type { Tool } from '@/app/components/tools/types'
|
||||||
|
import type { Model } from '@/types/app'
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import { v4 as uuid4 } from 'uuid'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
|
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
|
import { generateFlowchart } from '@/service/debug'
|
||||||
|
import {
|
||||||
|
useAllBuiltInTools,
|
||||||
|
useAllCustomTools,
|
||||||
|
useAllMCPTools,
|
||||||
|
useAllWorkflowTools,
|
||||||
|
} from '@/service/use-tools'
|
||||||
|
import { ModelModeType } from '@/types/app'
|
||||||
|
import { basePath } from '@/utils/var'
|
||||||
|
import {
|
||||||
|
CUSTOM_EDGE,
|
||||||
|
NODE_WIDTH,
|
||||||
|
NODE_WIDTH_X_OFFSET,
|
||||||
|
VIBE_COMMAND_EVENT,
|
||||||
|
} from '../constants'
|
||||||
|
import { BlockEnum } from '../types'
|
||||||
|
import {
|
||||||
|
generateNewNode,
|
||||||
|
getLayoutByDagre,
|
||||||
|
getNodeCustomTypeByNodeDataType,
|
||||||
|
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||||
|
} from '../utils'
|
||||||
|
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||||
|
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||||
|
import { useNodesReadOnly } from './use-workflow'
|
||||||
|
import { useWorkflowHistory, WorkflowHistoryEvent } from './use-workflow-history'
|
||||||
|
|
||||||
|
type VibeCommandDetail = {
|
||||||
|
dsl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParsedNodeDraft = {
|
||||||
|
id: string
|
||||||
|
type?: BlockEnum
|
||||||
|
title?: string
|
||||||
|
toolKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParsedNode = {
|
||||||
|
id: string
|
||||||
|
type: BlockEnum
|
||||||
|
title?: string
|
||||||
|
toolKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParsedEdge = {
|
||||||
|
sourceId: string
|
||||||
|
targetId: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseError = {
|
||||||
|
error: 'invalidMermaid' | 'missingNodeType' | 'unknownNodeType' | 'unknownTool' | 'missingNodeDefinition'
|
||||||
|
detail?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseResult = {
|
||||||
|
nodes: ParsedNode[]
|
||||||
|
edges: ParsedEdge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_DECLARATION = /^([A-Z][\w-]*)\s*\[(?:"([^"]+)"|([^\]]+))\]\s*$/i
|
||||||
|
const EDGE_DECLARATION = /^(.+?)\s*-->\s*(?:\|([^|]+)\|\s*)?(.+)$/
|
||||||
|
|
||||||
|
const extractMermaidCode = (raw: string) => {
|
||||||
|
const fencedMatch = raw.match(/```(?:mermaid)?\s*([\s\S]*?)```/i)
|
||||||
|
return (fencedMatch ? fencedMatch[1] : raw).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMermaidFlowchart = (value: string) => {
|
||||||
|
const trimmed = value.trim().toLowerCase()
|
||||||
|
return trimmed.startsWith('flowchart') || trimmed.startsWith('graph')
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeKey = (value: string) => value.trim().toLowerCase().replace(/[^\p{L}\p{N}]/gu, '')
|
||||||
|
|
||||||
|
const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
|
||||||
|
if (!icon)
|
||||||
|
return icon
|
||||||
|
if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`))
|
||||||
|
return `${basePath}${icon}`
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseNodeLabel = (label: string) => {
|
||||||
|
const tokens = label.split('|').map(token => token.trim()).filter(Boolean)
|
||||||
|
const info: Record<string, string> = {}
|
||||||
|
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
const [rawKey, ...rest] = token.split('=')
|
||||||
|
if (!rawKey || rest.length === 0)
|
||||||
|
return
|
||||||
|
info[rawKey.trim().toLowerCase()] = rest.join('=').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!info.type && tokens.length === 1 && !tokens[0].includes('=')) {
|
||||||
|
info.type = tokens[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseNodeToken = (token: string) => {
|
||||||
|
const trimmed = token.trim()
|
||||||
|
const match = trimmed.match(NODE_DECLARATION)
|
||||||
|
if (match)
|
||||||
|
return { id: match[1], label: match[2] || match[3] }
|
||||||
|
const idMatch = trimmed.match(/^([A-Z][\w-]*)$/i)
|
||||||
|
if (idMatch)
|
||||||
|
return { id: idMatch[1] }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMermaidFlowchart = (
|
||||||
|
raw: string,
|
||||||
|
nodeTypeLookup: Map<string, BlockEnum>,
|
||||||
|
toolLookup: Map<string, ToolDefaultValue>,
|
||||||
|
): ParseResult | ParseError => {
|
||||||
|
const code = extractMermaidCode(raw)
|
||||||
|
const lines = code.split(/\r?\n/).map((line) => {
|
||||||
|
const commentIndex = line.indexOf('%%')
|
||||||
|
return (commentIndex >= 0 ? line.slice(0, commentIndex) : line).trim()
|
||||||
|
}).filter(Boolean)
|
||||||
|
|
||||||
|
const nodesMap = new Map<string, ParsedNodeDraft>()
|
||||||
|
const edges: ParsedEdge[] = []
|
||||||
|
|
||||||
|
const registerNode = (id: string, label?: string): ParseError | null => {
|
||||||
|
const existing = nodesMap.get(id)
|
||||||
|
if (!label) {
|
||||||
|
if (!existing)
|
||||||
|
nodesMap.set(id, { id })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = parseNodeLabel(label)
|
||||||
|
if (!info.type)
|
||||||
|
return { error: 'missingNodeType', detail: label }
|
||||||
|
|
||||||
|
const typeKey = normalizeKey(info.type)
|
||||||
|
const nodeType = nodeTypeLookup.get(typeKey)
|
||||||
|
if (!nodeType)
|
||||||
|
return { error: 'unknownNodeType', detail: info.type }
|
||||||
|
|
||||||
|
const nodeData: ParsedNodeDraft = {
|
||||||
|
id,
|
||||||
|
type: nodeType,
|
||||||
|
title: info.title,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeType === BlockEnum.Tool) {
|
||||||
|
if (!info.tool)
|
||||||
|
return { error: 'unknownTool', detail: 'tool' }
|
||||||
|
const toolKey = normalizeKey(info.tool)
|
||||||
|
if (!toolLookup.has(toolKey))
|
||||||
|
return { error: 'unknownTool', detail: info.tool }
|
||||||
|
nodeData.toolKey = toolKey
|
||||||
|
}
|
||||||
|
|
||||||
|
nodesMap.set(id, { ...(existing || {}), ...nodeData })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.toLowerCase().startsWith('flowchart') || line.toLowerCase().startsWith('graph'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (line.includes('-->')) {
|
||||||
|
const edgeMatch = line.match(EDGE_DECLARATION)
|
||||||
|
if (!edgeMatch)
|
||||||
|
return { error: 'invalidMermaid', detail: line }
|
||||||
|
|
||||||
|
const sourceToken = parseNodeToken(edgeMatch[1])
|
||||||
|
const targetToken = parseNodeToken(edgeMatch[3])
|
||||||
|
if (!sourceToken || !targetToken)
|
||||||
|
return { error: 'invalidMermaid', detail: line }
|
||||||
|
|
||||||
|
const sourceError = registerNode(sourceToken.id, sourceToken.label)
|
||||||
|
if (sourceError)
|
||||||
|
return sourceError
|
||||||
|
const targetError = registerNode(targetToken.id, targetToken.label)
|
||||||
|
if (targetError)
|
||||||
|
return targetError
|
||||||
|
|
||||||
|
edges.push({
|
||||||
|
sourceId: sourceToken.id,
|
||||||
|
targetId: targetToken.id,
|
||||||
|
label: edgeMatch[2]?.trim() || undefined,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeMatch = line.match(NODE_DECLARATION)
|
||||||
|
if (nodeMatch) {
|
||||||
|
const error = registerNode(nodeMatch[1], nodeMatch[2] || nodeMatch[3])
|
||||||
|
if (error)
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedNodes: ParsedNode[] = []
|
||||||
|
for (const node of nodesMap.values()) {
|
||||||
|
if (!node.type)
|
||||||
|
return { error: 'missingNodeDefinition', detail: node.id }
|
||||||
|
parsedNodes.push(node as ParsedNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedNodes.length)
|
||||||
|
return { error: 'invalidMermaid', detail: '' }
|
||||||
|
|
||||||
|
return { nodes: parsedNodes, edges }
|
||||||
|
}
|
||||||
|
|
||||||
|
const dedupeHandles = (handles?: string[]) => {
|
||||||
|
if (!handles)
|
||||||
|
return handles
|
||||||
|
return Array.from(new Set(handles))
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBranchLabel = (label?: string) => {
|
||||||
|
if (!label)
|
||||||
|
return ''
|
||||||
|
const normalized = label.trim().toLowerCase()
|
||||||
|
if (['true', 'yes', 'y', '1'].includes(normalized))
|
||||||
|
return 'true'
|
||||||
|
if (['false', 'no', 'n', '0'].includes(normalized))
|
||||||
|
return 'false'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildToolParams = (parameters?: Tool['parameters']) => {
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
if (!parameters)
|
||||||
|
return params
|
||||||
|
parameters.forEach((item) => {
|
||||||
|
params[item.name] = ''
|
||||||
|
})
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWorkflowVibe = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const language = useGetLanguage()
|
||||||
|
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||||
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||||
|
const { getNodesReadOnly } = useNodesReadOnly()
|
||||||
|
const { saveStateToHistory } = useWorkflowHistory()
|
||||||
|
const { defaultModel } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||||
|
|
||||||
|
const { data: buildInTools } = useAllBuiltInTools()
|
||||||
|
const { data: customTools } = useAllCustomTools()
|
||||||
|
const { data: workflowTools } = useAllWorkflowTools()
|
||||||
|
const { data: mcpTools } = useAllMCPTools()
|
||||||
|
|
||||||
|
const [modelConfig, setModelConfig] = useState<Model | null>(null)
|
||||||
|
const isGeneratingRef = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedModel = (() => {
|
||||||
|
if (typeof window === 'undefined')
|
||||||
|
return null
|
||||||
|
const stored = localStorage.getItem('auto-gen-model')
|
||||||
|
if (!stored)
|
||||||
|
return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored) as Model
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (storedModel) {
|
||||||
|
setModelConfig(storedModel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultModel) {
|
||||||
|
setModelConfig({
|
||||||
|
name: defaultModel.model,
|
||||||
|
provider: defaultModel.provider.provider,
|
||||||
|
mode: ModelModeType.chat,
|
||||||
|
completion_params: {} as Model['completion_params'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [defaultModel])
|
||||||
|
|
||||||
|
const availableNodesList = useMemo(() => {
|
||||||
|
if (!nodesMetaDataMap)
|
||||||
|
return []
|
||||||
|
return Object.values(nodesMetaDataMap).map(node => ({
|
||||||
|
type: node.metaData.type,
|
||||||
|
title: node.metaData.title,
|
||||||
|
description: node.metaData.description,
|
||||||
|
}))
|
||||||
|
}, [nodesMetaDataMap])
|
||||||
|
|
||||||
|
const toolOptions = useMemo(() => {
|
||||||
|
const collections = [
|
||||||
|
buildInTools,
|
||||||
|
customTools,
|
||||||
|
workflowTools,
|
||||||
|
mcpTools,
|
||||||
|
].filter(Boolean) as ToolWithProvider[][]
|
||||||
|
|
||||||
|
const tools: ToolDefaultValue[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
collections.forEach((collection) => {
|
||||||
|
collection.forEach((provider) => {
|
||||||
|
provider.tools.forEach((tool) => {
|
||||||
|
const key = `${provider.id}:${tool.name}`
|
||||||
|
if (seen.has(key))
|
||||||
|
return
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
const params = buildToolParams(tool.parameters)
|
||||||
|
const toolDescription = typeof tool.description === 'object'
|
||||||
|
? tool.description?.[language]
|
||||||
|
: tool.description
|
||||||
|
tools.push({
|
||||||
|
provider_id: provider.id,
|
||||||
|
provider_type: provider.type,
|
||||||
|
provider_name: provider.name,
|
||||||
|
plugin_id: provider.plugin_id,
|
||||||
|
plugin_unique_identifier: provider.plugin_unique_identifier,
|
||||||
|
provider_icon: normalizeProviderIcon(provider.icon),
|
||||||
|
provider_icon_dark: normalizeProviderIcon(provider.icon_dark),
|
||||||
|
tool_name: tool.name,
|
||||||
|
tool_label: tool.label[language] || tool.name,
|
||||||
|
tool_description: toolDescription || '',
|
||||||
|
is_team_authorization: provider.is_team_authorization,
|
||||||
|
paramSchemas: tool.parameters,
|
||||||
|
params,
|
||||||
|
output_schema: tool.output_schema,
|
||||||
|
meta: provider.meta,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return tools
|
||||||
|
}, [buildInTools, customTools, workflowTools, mcpTools, language])
|
||||||
|
|
||||||
|
const toolLookup = useMemo(() => {
|
||||||
|
const map = new Map<string, ToolDefaultValue>()
|
||||||
|
toolOptions.forEach((tool) => {
|
||||||
|
const primaryKey = normalizeKey(`${tool.provider_id}/${tool.tool_name}`)
|
||||||
|
map.set(primaryKey, tool)
|
||||||
|
|
||||||
|
const providerNameKey = normalizeKey(`${tool.provider_name}/${tool.tool_name}`)
|
||||||
|
map.set(providerNameKey, tool)
|
||||||
|
|
||||||
|
const labelKey = normalizeKey(tool.tool_label)
|
||||||
|
map.set(labelKey, tool)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, [toolOptions])
|
||||||
|
|
||||||
|
const nodeTypeLookup = useMemo(() => {
|
||||||
|
const map = new Map<string, BlockEnum>()
|
||||||
|
if (!nodesMetaDataMap)
|
||||||
|
return map
|
||||||
|
Object.values(nodesMetaDataMap).forEach((node) => {
|
||||||
|
map.set(normalizeKey(node.metaData.type), node.metaData.type)
|
||||||
|
if (node.metaData.title)
|
||||||
|
map.set(normalizeKey(node.metaData.title), node.metaData.type)
|
||||||
|
})
|
||||||
|
map.set('ifelse', BlockEnum.IfElse)
|
||||||
|
map.set('ifelsecase', BlockEnum.IfElse)
|
||||||
|
return map
|
||||||
|
}, [nodesMetaDataMap])
|
||||||
|
|
||||||
|
const handleVibeCommand = useCallback(async (dsl?: string) => {
|
||||||
|
if (getNodesReadOnly()) {
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.readOnly') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = dsl?.trim() || ''
|
||||||
|
if (!trimmed) {
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.missingInstruction') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodesMetaDataMap || Object.keys(nodesMetaDataMap).length === 0) {
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.nodesUnavailable') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modelConfig && !isMermaidFlowchart(trimmed)) {
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.modelUnavailable') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGeneratingRef.current)
|
||||||
|
return
|
||||||
|
isGeneratingRef.current = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getNodes, setNodes, edges, setEdges } = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
|
||||||
|
const existingNodesPayload = nodes.map(node => ({
|
||||||
|
id: node.id,
|
||||||
|
type: node.data.type,
|
||||||
|
title: node.data.title || '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const toolsPayload = toolOptions.map(tool => ({
|
||||||
|
provider_id: tool.provider_id,
|
||||||
|
provider_name: tool.provider_name,
|
||||||
|
tool_name: tool.tool_name,
|
||||||
|
tool_label: tool.tool_label,
|
||||||
|
tool_key: `${tool.provider_id}/${tool.tool_name}`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const availableNodesPayload = availableNodesList.map(node => ({
|
||||||
|
type: node.type,
|
||||||
|
title: node.title,
|
||||||
|
description: node.description,
|
||||||
|
}))
|
||||||
|
|
||||||
|
let mermaidCode = trimmed
|
||||||
|
if (!isMermaidFlowchart(trimmed)) {
|
||||||
|
const { error, flowchart } = await generateFlowchart({
|
||||||
|
instruction: trimmed,
|
||||||
|
model_config: modelConfig,
|
||||||
|
available_nodes: availableNodesPayload,
|
||||||
|
existing_nodes: existingNodesPayload,
|
||||||
|
available_tools: toolsPayload,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Toast.notify({ type: 'error', message: error })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flowchart) {
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.missingFlowchart') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mermaidCode = flowchart
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResult = parseMermaidFlowchart(mermaidCode, nodeTypeLookup, toolLookup)
|
||||||
|
if ('error' in parseResult) {
|
||||||
|
switch (parseResult.error) {
|
||||||
|
case 'missingNodeType':
|
||||||
|
case 'missingNodeDefinition':
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') })
|
||||||
|
return
|
||||||
|
case 'unknownNodeType':
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.nodeTypeUnavailable', { type: parseResult.detail }) })
|
||||||
|
return
|
||||||
|
case 'unknownTool':
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.toolUnavailable', { tool: parseResult.detail }) })
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingStartNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||||
|
const newNodes: Node[] = []
|
||||||
|
const nodeIdMap = new Map<string, Node>()
|
||||||
|
|
||||||
|
parseResult.nodes.forEach((nodeSpec) => {
|
||||||
|
if (nodeSpec.type === BlockEnum.Start && existingStartNode) {
|
||||||
|
nodeIdMap.set(nodeSpec.id, existingStartNode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeDefault = nodesMetaDataMap[nodeSpec.type]
|
||||||
|
if (!nodeDefault)
|
||||||
|
return
|
||||||
|
|
||||||
|
const defaultValue = nodeDefault.defaultValue || {}
|
||||||
|
const title = nodeSpec.title?.trim() || nodeDefault.metaData.title || defaultValue.title || nodeSpec.type
|
||||||
|
|
||||||
|
const toolDefaultValue = nodeSpec.toolKey ? toolLookup.get(nodeSpec.toolKey) : undefined
|
||||||
|
const desc = (toolDefaultValue?.tool_description || (defaultValue as { desc?: string }).desc || '') as string
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...(defaultValue as Record<string, unknown>),
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
type: nodeSpec.type,
|
||||||
|
selected: false,
|
||||||
|
...(toolDefaultValue || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNode = generateNewNode({
|
||||||
|
id: uuid4(),
|
||||||
|
type: getNodeCustomTypeByNodeDataType(nodeSpec.type),
|
||||||
|
data,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
}).newNode
|
||||||
|
|
||||||
|
newNodes.push(newNode)
|
||||||
|
nodeIdMap.set(nodeSpec.id, newNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!newNodes.length) {
|
||||||
|
Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildEdge = (
|
||||||
|
source: Node,
|
||||||
|
target: Node,
|
||||||
|
sourceHandle = 'source',
|
||||||
|
targetHandle = 'target',
|
||||||
|
): Edge => ({
|
||||||
|
id: `${source.id}-${sourceHandle}-${target.id}-${targetHandle}`,
|
||||||
|
type: CUSTOM_EDGE,
|
||||||
|
source: source.id,
|
||||||
|
sourceHandle,
|
||||||
|
target: target.id,
|
||||||
|
targetHandle,
|
||||||
|
data: {
|
||||||
|
sourceType: source.data.type,
|
||||||
|
targetType: target.data.type,
|
||||||
|
isInIteration: false,
|
||||||
|
isInLoop: false,
|
||||||
|
_connectedNodeIsSelected: false,
|
||||||
|
},
|
||||||
|
zIndex: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const newEdges: Edge[] = []
|
||||||
|
parseResult.edges.forEach((edgeSpec) => {
|
||||||
|
const sourceNode = nodeIdMap.get(edgeSpec.sourceId)
|
||||||
|
const targetNode = nodeIdMap.get(edgeSpec.targetId)
|
||||||
|
if (!sourceNode || !targetNode)
|
||||||
|
return
|
||||||
|
|
||||||
|
let sourceHandle = 'source'
|
||||||
|
if (sourceNode.data.type === BlockEnum.IfElse) {
|
||||||
|
const branchLabel = normalizeBranchLabel(edgeSpec.label)
|
||||||
|
if (branchLabel === 'true') {
|
||||||
|
sourceHandle = (sourceNode.data as { cases?: { case_id: string }[] })?.cases?.[0]?.case_id || 'true'
|
||||||
|
}
|
||||||
|
if (branchLabel === 'false') {
|
||||||
|
sourceHandle = 'false'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newEdges.push(buildEdge(sourceNode, targetNode, sourceHandle))
|
||||||
|
})
|
||||||
|
|
||||||
|
const bounds = nodes.reduce(
|
||||||
|
(acc, node) => {
|
||||||
|
const width = node.width ?? NODE_WIDTH
|
||||||
|
acc.maxX = Math.max(acc.maxX, node.position.x + width)
|
||||||
|
acc.minY = Math.min(acc.minY, node.position.y)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{ maxX: 0, minY: 0 },
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseX = nodes.length ? bounds.maxX + NODE_WIDTH_X_OFFSET : 0
|
||||||
|
const baseY = Number.isFinite(bounds.minY) ? bounds.minY : 0
|
||||||
|
const branchOffset = Math.max(120, NODE_WIDTH_X_OFFSET / 2)
|
||||||
|
|
||||||
|
const layoutNodeIds = new Set(newNodes.map(node => node.id))
|
||||||
|
const layoutEdges = newEdges.filter(edge =>
|
||||||
|
layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target),
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const layout = await getLayoutByDagre(newNodes, layoutEdges)
|
||||||
|
const layoutedNodes = newNodes.map((node) => {
|
||||||
|
const info = layout.nodes.get(node.id)
|
||||||
|
if (!info)
|
||||||
|
return node
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
position: {
|
||||||
|
x: baseX + info.x,
|
||||||
|
y: baseY + info.y,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
newNodes.splice(0, newNodes.length, ...layoutedNodes)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
newNodes.forEach((node, index) => {
|
||||||
|
const row = Math.floor(index / 4)
|
||||||
|
const col = index % 4
|
||||||
|
node.position = {
|
||||||
|
x: baseX + col * NODE_WIDTH_X_OFFSET,
|
||||||
|
y: baseY + row * branchOffset,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const allNodes = [...nodes, ...newNodes]
|
||||||
|
const nodesConnectedMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||||
|
newEdges.map(edge => ({ type: 'add', edge })),
|
||||||
|
allNodes,
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedNodes = allNodes.map((node) => {
|
||||||
|
const connected = nodesConnectedMap[node.id]
|
||||||
|
if (!connected)
|
||||||
|
return node
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
...connected,
|
||||||
|
_connectedSourceHandleIds: dedupeHandles(connected._connectedSourceHandleIds),
|
||||||
|
_connectedTargetHandleIds: dedupeHandles(connected._connectedTargetHandleIds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setNodes(updatedNodes)
|
||||||
|
setEdges([...edges, ...newEdges])
|
||||||
|
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNodes[0].id })
|
||||||
|
handleSyncWorkflowDraft()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isGeneratingRef.current = false
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
availableNodesList,
|
||||||
|
getNodesReadOnly,
|
||||||
|
handleSyncWorkflowDraft,
|
||||||
|
modelConfig,
|
||||||
|
nodeTypeLookup,
|
||||||
|
nodesMetaDataMap,
|
||||||
|
saveStateToHistory,
|
||||||
|
store,
|
||||||
|
t,
|
||||||
|
toolLookup,
|
||||||
|
toolOptions,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (event: CustomEvent<VibeCommandDetail>) => {
|
||||||
|
handleVibeCommand(event.detail?.dsl)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener(VIBE_COMMAND_EVENT, handler as EventListener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(VIBE_COMMAND_EVENT, handler as EventListener)
|
||||||
|
}
|
||||||
|
}, [handleVibeCommand])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@ -68,6 +68,7 @@ import {
|
|||||||
useWorkflow,
|
useWorkflow,
|
||||||
useWorkflowReadOnly,
|
useWorkflowReadOnly,
|
||||||
useWorkflowRefreshDraft,
|
useWorkflowRefreshDraft,
|
||||||
|
useWorkflowVibe,
|
||||||
} from './hooks'
|
} from './hooks'
|
||||||
import { HooksStoreContextProvider, useHooksStore } from './hooks-store'
|
import { HooksStoreContextProvider, useHooksStore } from './hooks-store'
|
||||||
import { useWorkflowSearch } from './hooks/use-workflow-search'
|
import { useWorkflowSearch } from './hooks/use-workflow-search'
|
||||||
@ -319,6 +320,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||||||
useShortcuts()
|
useShortcuts()
|
||||||
// Initialize workflow node search functionality
|
// Initialize workflow node search functionality
|
||||||
useWorkflowSearch()
|
useWorkflowSearch()
|
||||||
|
useWorkflowVibe()
|
||||||
|
|
||||||
// Set up scroll to node event listener using the utility function
|
// Set up scroll to node event listener using the utility function
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -333,6 +333,9 @@ const translation = {
|
|||||||
feedbackDesc: 'Open community feedback discussions',
|
feedbackDesc: 'Open community feedback discussions',
|
||||||
zenTitle: 'Zen Mode',
|
zenTitle: 'Zen Mode',
|
||||||
zenDesc: 'Toggle canvas focus mode',
|
zenDesc: 'Toggle canvas focus mode',
|
||||||
|
vibeTitle: 'Vibe Workflow',
|
||||||
|
vibeDesc: 'Generate a workflow from natural language',
|
||||||
|
vibeHint: 'Describe the workflow, e.g. "{{prompt}}"',
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
noAppsFound: 'No apps found',
|
noAppsFound: 'No apps found',
|
||||||
|
|||||||
@ -123,6 +123,16 @@ const translation = {
|
|||||||
noHistory: 'No History',
|
noHistory: 'No History',
|
||||||
tagBound: 'Number of apps using this tag',
|
tagBound: 'Number of apps using this tag',
|
||||||
},
|
},
|
||||||
|
vibe: {
|
||||||
|
readOnly: 'This workflow is read-only.',
|
||||||
|
missingInstruction: 'Describe the workflow you want to build.',
|
||||||
|
modelUnavailable: 'No model available for flowchart generation.',
|
||||||
|
nodesUnavailable: 'Workflow nodes are not available yet.',
|
||||||
|
missingFlowchart: 'No flowchart was generated.',
|
||||||
|
invalidFlowchart: 'The generated flowchart could not be parsed.',
|
||||||
|
nodeTypeUnavailable: 'Node type "{{type}}" is not available in this workflow.',
|
||||||
|
toolUnavailable: 'Tool "{{tool}}" is not available in this workspace.',
|
||||||
|
},
|
||||||
publishLimit: {
|
publishLimit: {
|
||||||
startNodeTitlePrefix: 'Upgrade to',
|
startNodeTitlePrefix: 'Upgrade to',
|
||||||
startNodeTitleSuffix: 'unlock unlimited triggers per workflow',
|
startNodeTitleSuffix: 'unlock unlimited triggers per workflow',
|
||||||
|
|||||||
@ -19,6 +19,11 @@ export type GenRes = {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FlowchartGenRes = {
|
||||||
|
flowchart: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type CodeGenRes = {
|
export type CodeGenRes = {
|
||||||
code: string
|
code: string
|
||||||
language: string[]
|
language: string[]
|
||||||
@ -93,6 +98,12 @@ export const generateRule = (body: Record<string, any>) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateFlowchart = (body: Record<string, any>) => {
|
||||||
|
return post<FlowchartGenRes>('/flowchart-generate', {
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchModelParams = (providerName: string, modelId: string) => {
|
export const fetchModelParams = (providerName: string, modelId: string) => {
|
||||||
return get(`workspaces/current/model-providers/${providerName}/models/parameter-rules`, {
|
return get(`workspaces/current/model-providers/${providerName}/models/parameter-rules`, {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user