Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 139 additions & 4 deletions ui/src/views/details-demo/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import type { ExecutionNodeDetail } from '@/workflow-canvas/details/types'

defineOptions({ name: 'DetailsDemo' })

// 一个 AI 对话节点的示例执行详情。
const mode = ref<WorkflowMode>(WorkflowMode.Application)

function base(type: WorkflowNodeType, index: number, name: string, overrides: Partial<ExecutionNodeDetail> = {}): ExecutionNodeDetail {
return { type, name, status: 200, index, run_time: 0.86, ...overrides }
}

function makeAiChat(overrides: Partial<ExecutionNodeDetail> = {}): ExecutionNodeDetail {
return {
type: WorkflowNodeType.AiChat,
Expand All @@ -30,12 +35,144 @@ function makeAiChat(overrides: Partial<ExecutionNodeDetail> = {}): ExecutionNode
}

const detail = ref<ExecutionNodeDetail[]>([
// ===== AI 对话 =====
makeAiChat({ index: 0 }),

// ===== 判断器 =====
base(WorkflowNodeType.Condition, 1, '判断器', {
branch_name: '包含关键词「退款」',
}),

// ===== 指定回复 =====
base(WorkflowNodeType.Reply, 2, '指定回复', {
answer: '抱歉,该问题暂时无法回答,请换个问法。',
}),

// ===== 开始节点 =====
base(WorkflowNodeType.Start, 3, '开始', {
question: '帮我查一下报销流程',
global_fields: [{ label: '部门', value: '财务部' }],
document_list: [{ name: '报销制度.pdf' }],
}),

// ===== 问题优化 =====
base(WorkflowNodeType.Question, 4, '问题优化', {
system: '你负责优化用户问题,使其更清晰、更利于检索。',
history_message: [{ role: 'user', content: '报销流程' }],
question: 'MaxKB 的报销流程是什么?',
answer: '报销流程包括:提交申请 → 部门审批 → 财务打款。',
}),

// ===== 意图识别 =====
base(WorkflowNodeType.IntentNode, 5, '意图识别', {
system: '判断用户意图。',
question: '我想申请报销',
answer: '报销申请',
}),

// ===== 文档内容提取 =====
base(WorkflowNodeType.DocumentExtractNode, 6, '文档内容提取', {
content: ['## 第一章\n这里是提取出的文档正文内容。\n\n- 要点一\n- 要点二'],
}),

// ===== 参数提取 =====
base(WorkflowNodeType.ParameterExtractionNode, 7, '参数提取', {
request: '从用户问题中提取姓名和金额。',
result: { name: '张三', amount: '1200' },
}),

// ===== 变量赋值 =====
base(WorkflowNodeType.VariableAssignNode, 8, '变量赋值', {
result_list: [
{ name: 'amount', input_type: 'number', input_value: '1200', output_type: 'string', output_value: '1200 元' },
],
}),

// ===== 工具 =====
base(WorkflowNodeType.ToolLibCustom, 9, '自定义工具', {
index: 1,
params: JSON.stringify({ city: '上海' }),
result: JSON.stringify({ weather: '晴,26℃' }),
}),

// ===== MCP 节点 =====
base(WorkflowNodeType.McpNode, 10, 'MCP 工具', {
mcp_tool: 'github.get_repo',
tool_params: { owner: '1Panel-dev', repo: 'MaxKB' },
result: ['星标数: 15000'],
}),

// ===== 图片理解 =====
base(WorkflowNodeType.ImageUnderstandNode, 11, '图片理解', {
system: '识别图片内容。',
question: '这张图里有什么?',
reasoning_content: '识别到一辆红色轿车停在停车场。',
answer: '图中是一辆红色轿车。',
}),

// ===== 图片生成 =====
base(WorkflowNodeType.ImageGenerateNode, 12, '图片生成', {
question: '生成一张极简风格的 Logo。',
negative_prompt: '避免复杂花纹、避免文字。',
answer: '![logo](data:image/svg+xml;base64,)',
}),

// ===== 知识库检索 =====
base(WorkflowNodeType.SearchKnowledge, 13, '知识库检索', {
question: 'MaxKB 支持哪些部署方式?',
paragraph_list: [
{
title: '部署方式介绍',
content: 'MaxKB 支持 Docker、Kubernetes 与裸机部署。',
similarity: 0.92,
document_name: '部署文档.pdf',
knowledge_name: '产品手册',
knowledge_type: 'common',
},
{
title: '系统要求',
content: '最低 2 核 4G 内存,推荐 4 核 8G。',
similarity: 0.78,
document_name: '部署文档.pdf',
knowledge_name: '产品手册',
knowledge_type: 'common',
},
],
}),

// ===== 多路召回 =====
base(WorkflowNodeType.RerankerNode, 14, '多路召回', {
question: 'MaxKB 的检索机制',
document_list: [
{
page_content: '基于关键词与向量混合检索,融合多路召回结果。',
metadata: { title: '混合检索', similarity: 0.85, document_name: '架构说明.md', knowledge_name: '开发文档' },
},
],
result_list: [
{
page_content: '基于关键词与向量混合检索,融合多路召回结果。',
metadata: { title: '混合检索', relevance_score: 0.93, document_name: '架构说明.md', knowledge_name: '开发文档' },
},
],
}),

// ===== 表单收集 =====
base(WorkflowNodeType.FormNode, 15, '表单收集', {
form_field_list: [
{ field: 'name', label: '姓名', input_type: 'input', value: '' },
{ field: 'department', label: '部门', input_type: 'input', value: '' },
],
form_data: { name: '张三', department: '财务部' },
is_submit: true,
}),

// ===== 循环 =====
{
type: WorkflowNodeType.LoopNode,
name: '循环',
status: 200,
index: 1,
index: 16,
run_time: 4.567,
loop_type: 'ARRAY',
loop_node_data: {
Expand All @@ -62,8 +199,6 @@ const detail = ref<ExecutionNodeDetail[]>([
},
} as ExecutionNodeDetail,
])

const mode = ref<WorkflowMode>(WorkflowMode.Application)
</script>

<template>
Expand Down
6 changes: 6 additions & 0 deletions ui/src/workflow-canvas/WORKFLOW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,12 @@ MkFormList 的排序、增删均以 `cloneDeep` 回写;MkTable 保留普通行
- `NodeMenu` 使用 Element Plus Tabs 组织基础组件、工具和智能体;基础组件直接渲染 `node-menu/menu.ts` 返回的菜单分组,工具和智能体复用 Workspace 文件夹树加载可用资源。画布右上角的 `AddNode` 和节点锚点菜单均支持点击创建与拖拽到画布创建。
- `ApplicationWorkflowView` 已接入详情加载、默认工作流、手动与自动保存、发布以及未保存退出确认。
- `ToolWorkflowView` 已接入工具模式、工具专属默认节点、详情加载、手动保存以及未保存退出确认。
- 执行详情已覆盖:判断器、指定回复、开始/应用、问题优化、意图识别、文档内容提取、参数提取、变量赋值、
变量拆分、变量聚合、工具/自定义工具、MCP、工具开始、工具工作流、循环开始/继续/退出、图片理解、
视频理解、图片生成、文生视频、图生视频。其中开始/应用、问题优化、意图识别为单文件;图片理解与视频理解
按 `application.vue`/`knowledge.vue` 拆包;多路召回等以 `ParagraphCard` 渲染段落的节点(知识库检索、
多路召回、文档分段、知识库写入)与表单收集节点暂未迁移。文件类型图标复用 `@/utils/icon` 的
`getFileIconUrl`。
- 工具工作流调试和发布、发布历史、模板中心、完整国际化、枚举补充和其他尚未迁入的节点组件后续再接入。

## 检查
Expand Down
123 changes: 123 additions & 0 deletions ui/src/workflow-canvas/nodes/application-node/details/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<script setup lang="ts">
import DetailContainer from '@/workflow-canvas/details/DetailContainer.vue'
import BaseHeader from '@/workflow-canvas/details/BaseHeader.vue'
import { getFileIconUrl } from '@/utils/icon'
import type { ExecutionNodeDetail } from '@/workflow-canvas/details/types'

defineOptions({ name: 'ApplicationNodeDetail' })

defineProps<{
data: ExecutionNodeDetail
}>()
</script>

<template>
<DetailContainer :data="data">
<template #header="{ show }">
<BaseHeader :data="data" :show="show" />
</template>

<div class="overflow-hidden rounded-md bg-N100">
<h5 class="px-3 py-2">输入参数</h5>
<div class="border-t border-dashed px-3 py-2 text-N900">
<p class="mb-2"><span class="mr-1 text-N600">问题:</span>{{ data.question || '-' }}</p>

<p v-for="(f, i) in data.global_fields" :key="i" class="mb-2">
<span class="mr-1 text-N600">{{ f.label }}:</span>{{ f.value }}
</p>

<div v-if="data.document_list?.length > 0" class="mb-2">
<p class="mb-1 text-N600">文档:</p>
<div class="flex flex-wrap gap-2">
<div
v-for="(f, i) in data.document_list"
:key="i"
class="flex items-center rounded-md bg-white px-2 py-1"
>
<img :src="getFileIconUrl(f?.name)" alt="" width="24" />
<span class="ml-1 max-w-[200px] truncate" :title="f?.name">{{ f?.name }}</span>
</div>
</div>
</div>

<div v-if="data.image_list?.length > 0" class="mb-2">
<p class="mb-1 text-N600">图片:</p>
<div class="flex flex-wrap gap-2">
<el-image
v-for="(f, i) in data.image_list"
:key="i"
:src="f.url"
fit="cover"
class="block h-10 w-10 rounded-md"
:preview-src-list="data.image_list.map((img: any) => img.url)"
:initial-index="i"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
/>
</div>
</div>

<div v-if="data.audio_list?.length > 0" class="mb-2">
<p class="mb-1 text-N600">音频文件:</p>
<audio
v-for="(f, i) in data.audio_list"
:key="i"
:src="f.url"
controls
class="rounded-md"
style="width: 300px; height: 43px"
/>
</div>

<div v-if="data.video_list?.length > 0" class="mb-2">
<p class="mb-1 text-N600">视频:</p>
<video
v-for="(f, i) in data.video_list"
:key="i"
:src="f.url"
controls
autoplay
class="block rounded-md"
style="width: 170px"
/>
</div>

<div v-if="data.other_list?.length > 0" class="mb-2">
<p class="mb-1 text-N600">其他:</p>
<div class="flex flex-wrap gap-2">
<div
v-for="(f, i) in data.other_list"
:key="i"
class="flex items-center rounded-md bg-white px-2 py-1"
>
<img :src="getFileIconUrl(f?.name)" alt="" width="24" />
<span class="ml-1 max-w-[200px] truncate" :title="f?.name">{{ f?.name }}</span>
</div>
</div>
</div>
</div>
</div>

<div class="overflow-hidden rounded-md bg-N100">
<h5 class="px-3 py-2">输出参数</h5>
<div class="border-t border-dashed px-3 py-2 text-N900">
<MdPreview
v-if="data.answer"
editor-id="preview-only"
:model-value="data.answer"
class="application-answer"
noImgZoomIn
/>
<template v-else>-</template>
</div>
</div>
</DetailContainer>
</template>

<style scoped lang="scss">
:deep(.application-answer.md-editor) {
background: transparent;
height: auto;
}
</style>
3 changes: 2 additions & 1 deletion ui/src/workflow-canvas/nodes/application-node/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ApplicationNode from './index.vue'
import ApplicationNodeDetail from './details/index.vue'
import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node'
import { WorkflowNodeType } from '@/workflow-canvas/types'

Expand All @@ -8,4 +9,4 @@ class ApplicationNodeView extends WorkflowNodeView {
}
}

export default { type: WorkflowNodeType.Application, model: WorkflowNodeModel, view: ApplicationNodeView }
export default { type: WorkflowNodeType.Application, model: WorkflowNodeModel, view: ApplicationNodeView, details: ApplicationNodeDetail }
24 changes: 24 additions & 0 deletions ui/src/workflow-canvas/nodes/condition-node/details/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import DetailContainer from '@/workflow-canvas/details/DetailContainer.vue'
import BaseHeader from '@/workflow-canvas/details/BaseHeader.vue'
import type { ExecutionNodeDetail } from '@/workflow-canvas/details/types'

defineOptions({ name: 'ConditionNodeDetail' })

defineProps<{
data: ExecutionNodeDetail
}>()
</script>

<template>
<DetailContainer :data="data">
<template #header="{ show }">
<BaseHeader :data="data" :show="show" />
</template>

<div class="overflow-hidden rounded-md bg-N100">
<h5 class="px-3 py-2">条件结果</h5>
<div class="border-t border-dashed px-3 py-2 text-N900">{{ data.branch_name || '-' }}</div>
</div>
</DetailContainer>
</template>
3 changes: 2 additions & 1 deletion ui/src/workflow-canvas/nodes/condition-node/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ConditionNodeVue from './index.vue'
import ConditionNodeDetail from './details/index.vue'
import { type BaseEdgeModel, type Model } from '@logicflow/core'
import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node'
import { WorkflowNodeType } from '@/workflow-canvas/types'
Expand Down Expand Up @@ -71,4 +72,4 @@ class ConditionModel extends WorkflowNodeModel {
}
}

export default { type: WorkflowNodeType.Condition, model: ConditionModel, view: ConditionNodeView }
export default { type: WorkflowNodeType.Condition, model: ConditionModel, view: ConditionNodeView, details: ConditionNodeDetail }
Loading
Loading