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
8 changes: 7 additions & 1 deletion ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -763,11 +763,17 @@ Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选
`business/generate-content/index.vue` 封装生成入口按钮和弹窗,供提示词与 Python
代码生成复用,不通过 `mode` 判断业务。`title`、`placeholder`、`emptyText` 提供文案,
`disabled` 统一控制入口、发送和重新生成,生成期间由内部 `loading` 阻止重复提交。
工具侧在弹窗关闭时允许打开选模型,打开后根据模型及参数加载状态设置 `disabled`。
工具侧通过 `generateDisabled` 在模型未就绪时禁止提交,入口仍允许打开选模型。
`request(messages)` 接收 `PromptGenerateMessage[]` 并返回原始 `Promise<Response>`,调用方负责
选择 API、模板和请求参数。公共组件统一使用 `ConversationStream` 处理流式结果、错误、停止、
重新生成和会话清理,不感知工具表单、资源范围或模型接口。

可选 `modelOptions` 启用输入框底部的 `SelectModel`,通过 `v-model:model-id`、
`v-model:model-params` 回写生成模型及参数;`providerOptions` 提供供应商,`modelLoading`、
`modelParamsLoading` 表示模型列表和参数加载状态。调用方提供参数表单查询注入。
`canAddModel` 开启添加模型入口,创建后通过 `refreshModels` 事件通知调用方刷新选项。
`generateDisabled` 只禁用发送和重新生成,允许先打开弹窗选择模型。

`header-extra` 插槽提供 `{ loading }`,用于放置模型选择等业务设置;`open` 事件初始化业务上下文,
`closed` 在关闭动画结束后清理业务状态,`replace(content)` 交由调用方处理结果并回写。
重新生成追加用户消息 `Re generate` 后请求;停止调用当前流的 `cancel()`,保留已生成内容。
Expand Down
40 changes: 34 additions & 6 deletions ui/src/components/business/generate-content/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, reactive, ref, useTemplateRef, watch } from 'vue'
import type { ScrollbarInstance } from 'element-plus'
import type { PromptGenerateMessage } from '@/api/types'
import SelectModel from '@/components/business/select-model/index.vue'
import type { ModelItem, ModelProviderItem, PromptGenerateMessage } from '@/api/types'
import { ConversationStream } from '@/conversation-panel/core/stream'
import { inputShortcut } from '@/conversation-panel/core/shortcuts'
import { MsgError } from '@/utils/message'
Expand All @@ -14,19 +15,29 @@ const props = withDefaults(
placeholder: string
emptyText: string
disabled?: boolean
generateDisabled?: boolean
canAddModel?: boolean
modelOptions?: ModelItem[]
providerOptions?: ModelProviderItem[]
modelLoading?: boolean
modelParamsLoading?: boolean
request: (messages: PromptGenerateMessage[]) => Promise<Response>
}>(),
{ disabled: false },
{ disabled: false, generateDisabled: false },
)
const emit = defineEmits<{
open: []
closed: []
replace: [content: string]
refreshModels: []
}>()
defineSlots<{
'header-extra'(props: { loading: boolean }): unknown
}>()

const modelId = defineModel<string>('modelId', { default: '' })
const modelParams = defineModel<Record<string, unknown>>('modelParams', { default: () => ({}) })

// 生成会话:业务方提供请求上下文,组件统一维护输入、消息和弹窗。
const visible = ref(false)
const loading = ref(false)
Expand All @@ -53,7 +64,7 @@ function resetData() {
// 流式生成:重新生成追加用户消息,停止后保留已生成的内容。
function generateContent(regenerate = false) {
const content = inputValue.value.trim()
if (loading.value || props.disabled || (!regenerate && !content)) return
if (loading.value || props.disabled || props.generateDisabled || (!regenerate && !content)) return
messages.value.push({ content: regenerate ? 'Re generate' : content, role: 'user' })
const requestMessages = [...messages.value]
inputValue.value = ''
Expand Down Expand Up @@ -148,7 +159,7 @@ onBeforeUnmount(stopGenerate)
<!-- 替换当前内容 -->
<el-button type="primary" @click="replaceContent">替换</el-button>
<!-- 重新生成内容 -->
<el-button plain :disabled="disabled" @click="generateContent(true)">重新生成</el-button>
<el-button plain :disabled="disabled || generateDisabled" @click="generateContent(true)">重新生成</el-button>
</div>
<div class="mk-input-box border transition-colors hover:border-primary focus-within:border-primary">
<el-input
Expand All @@ -161,13 +172,30 @@ onBeforeUnmount(stopGenerate)
@keydown.stop="handleKeydown"
@paste.stop
/>
<div class="text-right mt-3">
<div class="flex-between gap-3 mt-3">
<div class="min-w-0">
<!-- 选择生成内容使用的模型及参数 -->
<SelectModel
v-if="modelOptions"
v-model="modelId"
v-model:model-params="modelParams"
class="w-60!"
:options="modelOptions"
:provider-options="providerOptions ?? []"
:loading="modelLoading"
:disabled="loading || modelParamsLoading"
:can-add="canAddModel"
can-edit-params
teleported
@refresh="emit('refreshModels')"
/>
</div>
<!-- 停止生成内容 -->
<el-button v-if="loading" circle type="primary" @click="stopGenerate">
<MkIcon name="icon_square_filled" />
</el-button>
<!-- 发送需求并生成内容 -->
<el-button v-else circle type="primary" :disabled="!inputValue.trim() || disabled" @click="generateContent()">
<el-button v-else circle type="primary" :disabled="!inputValue.trim() || disabled || generateDisabled" @click="generateContent()">
<MkIcon name="icon_arrow-up_outlined" />
</el-button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ const getData = () => {
}
const render = (formData: DynamicFormValue) => {
const attrs = formData.attrs || {}
formValue.value.minlength = attrs.minlength
formValue.value.maxlength = attrs.maxlength
formValue.value.default_value = formData.default_value
formValue.value.show_default_value = formData.show_default_value
formValue.value.minlength = attrs.minlength ?? 0
formValue.value.maxlength = attrs.maxlength ?? 200
formValue.value.default_value = formData.default_value ?? ''
formValue.value.show_default_value = formData.show_default_value ?? true
}
const rules = computed(() => [
{
Expand Down
7 changes: 7 additions & 0 deletions ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@
.icon-arrow-down {
font-size: 16px !important;
}
.el-cascader__tags {
.el-tag {
height: 24px !important;
align-items: center !important;
font-size: 14px;
}
}
}

.el-cascader-panel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ watch([() => props.workspaceIds, () => props.workspaceOptions], refreshUserGroup

<template>
<el-form-item>
<el-cascader v-model="userGroupIds" class="w-full" :options="userGroupOptions" :props="cascaderProps" :show-all-levels="false" clearable placeholder="请选择用户组" />
<el-cascader
v-model="userGroupIds"
class="w-full"
:options="userGroupOptions"
:props="cascaderProps"
:show-all-levels="false"
clearable
filterable
placeholder="请选择用户组"
/>
</el-form-item>
</template>
66 changes: 22 additions & 44 deletions ui/src/views/tool/tool-form/component/python-code/CodeGenerate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import ModelApi from '@/api/admin/workspace/model'
import SystemResourceModelApi from '@/api/admin/system/resource-management/model'
import SystemSharedModelApi from '@/api/admin/system/shared-resources/model'
import ModelProviderApi from '@/api/admin/model-provider'
import SelectModel from '@/components/business/select-model/index.vue'
import GenerateContent from '@/components/business/generate-content/index.vue'
import { MODEL_STATUS } from '@/api/enums'
import type {
Expand Down Expand Up @@ -113,19 +112,16 @@ function loadModels() {
// 生成上下文:打开时复制当前参数定义,关闭后清理模型配置。
const initFieldList = ref<DynamicFormField[]>([])
const inputFieldList = ref<ToolInputField[]>([])
const generateVisible = ref(false)
// 允许先打开弹窗选模型,打开后统一通过 disabled 控制提交。
const generateDisabled = computed(() => generateVisible.value && (!activeModelId.value || modelLoading.value || modelParamsLoading.value))
// 模型和参数就绪后允许提交,入口始终可打开。
const generateDisabled = computed(() => !activeModelId.value || modelLoading.value || modelParamsLoading.value)

function initGenerate() {
generateVisible.value = true
initFieldList.value = cloneDeep(props.toolForm?.init_field_list ?? [])
inputFieldList.value = cloneDeep(props.toolForm?.input_field_list ?? [])
return loadModels()
}

function resetData() {
generateVisible.value = false
initFieldList.value = []
inputFieldList.value = []
activeModelId.value = ''
Expand Down Expand Up @@ -171,29 +167,20 @@ function handleReplaceCode(value: string, replaceCode?: (value: string) => void)
title="生成 Python 代码"
placeholder="请描述这个工具的功能"
empty-text="Python 代码显示在这里"
:disabled="generateDisabled"
:generate-disabled="generateDisabled"
v-model:model-id="activeModelId"
v-model:model-params="modelParams"
:model-options="modelOptions"
:provider-options="providerOptions"
:model-loading="modelLoading"
:model-params-loading="modelParamsLoading"
can-add-model
@refresh-models="loadModels"
:request="requestGenerateCode"
@open="initGenerate"
@closed="resetData"
@replace="handleReplaceCode"
>
<template #header-extra="{ loading }">
<div class="flex-align-center gap-4 w-70!">
<SelectModel
v-model="activeModelId"
v-model:model-params="modelParams"
:options="modelOptions"
:provider-options="providerOptions"
:disabled="loading || modelLoading || modelParamsLoading"
can-edit-params
can-add
teleported
@refresh="loadModels"
/>
<el-divider direction="vertical" />
</div>
</template>
</GenerateContent>
/>
</div>

<el-form-item prop="code">
Expand All @@ -204,29 +191,20 @@ function handleReplaceCode(value: string, replaceCode?: (value: string) => void)
title="生成 Python 代码"
placeholder="请描述这个工具的功能"
empty-text="Python 代码显示在这里"
:disabled="generateDisabled"
:generate-disabled="generateDisabled"
v-model:model-id="activeModelId"
v-model:model-params="modelParams"
:model-options="modelOptions"
:provider-options="providerOptions"
:model-loading="modelLoading"
:model-params-loading="modelParamsLoading"
can-add-model
@refresh-models="loadModels"
:request="requestGenerateCode"
@open="initGenerate"
@closed="resetData"
@replace="handleReplaceCode($event, replaceCode)"
>
<template #header-extra="{ loading }">
<div class="flex-align-center gap-4 w-70!">
<SelectModel
v-model="activeModelId"
v-model:model-params="modelParams"
:options="modelOptions"
:provider-options="providerOptions"
:disabled="loading || modelLoading || modelParamsLoading"
can-edit-params
can-add
teleported
@refresh="loadModels"
/>
<el-divider direction="vertical" />
</div>
</template>
</GenerateContent>
/>
</template>
</PythonCodeEditor>
</el-form-item>
Expand Down
10 changes: 6 additions & 4 deletions ui/src/workflow-canvas/WORKFLOW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,17 +328,19 @@ ID 数组及 `tool_list`、`skill_tool_list`、`application_list` 回显快照
读取资源配置、通过 `update` 提交局部变更;资源选项由 Props 传入,未接入数据源时默认为空数组。

AI 对话节点的提示词、历史记录、视觉理解和输出思考表单直接在节点入口维护,统一使用全局
`MdEditorMagnify` 和节点表单样式。AI 对话、图片理解和视频理解的系统提示词生成统一使用
`MdEditorMagnify` 和节点表单样式。AI 对话、LLM、图片理解和视频理解的系统提示词生成统一使用
`workflow-canvas/component/PromptGenerate.vue`,内部复用公共业务组件 `GenerateContent`。
节点只传 `modelId` 并监听 `replace` 回写自己的 `system`;默认模型从当前工作流配置读取,
自定义模型使用节点配置的模型 ID,引用变量无法在编辑时确定模型,生成入口禁用。
自定义模型使用节点配置的模型 ID;引用变量无法在编辑时确定模型时,可在生成弹窗中选择。
图片理解和视频理解使用 IMAGE 模型,AI 对话使用 LLM 模型,现有提示词接口支持这两种类型。
共享组件维护提示词模板和请求上下文,不依赖某个节点,也不接收未使用的模型或供应商选项。
组件将画布注入的 `resourceScope` 传给 `useWorkflowStore`,通过 `store.postPromptGenerate` 发起生成;
Store 适配器统一选择 Workspace、System 资源管理或 System 共享资源的 Model API。
循环画布已转发同一上下文,组件不再单独判断资源范围。生成方法不使用 Store 的查询缓存或请求去重。
生成接口使用节点自身已保存的模型参数(`model_params_setting`)并随请求体提交,
因此智能体、工具和知识库工作流均可使用;弹窗不提供独立模型或参数设置。
打开弹窗时优先选中传入的节点模型并复制参数;未传模型 ID 时,按下拉分组顺序选中第一个可用模型并加载默认参数。
输入框底部复用 `SelectModel`,可独立调整本次生成使用的模型和参数,
不回写节点配置。选项通过当前范围 Store 查询 LLM 和 IMAGE 模型,生成请求提交弹窗当前的
`model_params_setting`;未选模型或选项、参数加载期间禁止发送。
公共组件统一维护主题输入、会话消息与 `ConversationStream`;重新生成在历史中追加用户消息
`Re generate` 后请求,停止通过 `cancel()` 终止读取,关闭动画结束后清理会话。
点击替换后通过 `replace` 交由节点回写系统提示词。
Expand Down
65 changes: 57 additions & 8 deletions ui/src/workflow-canvas/component/PromptGenerate.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { inject, ref } from 'vue'
import { inject, provide, ref } from 'vue'
import { useWorkflowStore } from '@/workflow-canvas/store'
import type { PromptGenerateMessage } from '@/api/types'
import { cloneDeep, groupBy } from 'lodash'
import { MODEL_STATUS } from '@/api/enums'
import type { ModelItem, ModelProviderItem, PromptGenerateMessage } from '@/api/types'
import GenerateContent from '@/components/business/generate-content/index.vue'
defineOptions({ name: 'PromptGenerate' })
const props = defineProps<{
modelId: string
modelId?: string
modelParamsSetting?: Record<string, unknown>
}>()
const emit = defineEmits<{ replace: [prompt: string] }>()
Expand Down Expand Up @@ -78,33 +80,80 @@ const PROMPT_TEMPLATE = `请根据用户描述生成一个完整的AI角色人

输出时不得包含任何解释或附加说明,只能返回符合以上格式的内容。`

// 生成上下文:沿用节点模型和节点自身已保存的模型参数。
// 生成上下文:复制节点模型配置,弹窗内独立选择生成模型和参数。
const activeModelId = ref('')
const modelParams = ref<Record<string, unknown>>({})
const modelOptions = ref<ModelItem[]>([])
const providerOptions = ref<ModelProviderItem[]>([])
const modelLoading = ref(false)
const modelParamsLoading = ref(false)

provide('getModelParamsForm', (modelId: string) => {
modelParamsLoading.value = true
return store.getModelParamsForm(modelId).finally(() => {
modelParamsLoading.value = false
})
})

function loadModels() {
modelLoading.value = true
return Promise.all([store.getModelListWithShared(), store.getProviderList()])
.then(([models, providers]) => {
modelOptions.value = models.filter(({ model_type }) => model_type === 'LLM' || model_type === 'IMAGE')
providerOptions.value = providers
if (activeModelId.value) return
// 未指定模型时,按下拉分组顺序选中首个可用模型并加载默认参数。
const firstModel = Object.values(groupBy(modelOptions.value, 'provider'))
.flat()
.find(({ status }) => status === MODEL_STATUS.SUCCESS)
if (!firstModel) return
activeModelId.value = firstModel.id
return store.getModelParamsForm(firstModel.id).then((fields) => {
if (activeModelId.value !== firstModel.id) return
modelParams.value = fields.reduce<Record<string, unknown>>((settings, field) => {
if (field.show_default_value !== false) settings[field.field] = cloneDeep(field.default_value)
return settings
}, {})
})
})
.finally(() => {
modelLoading.value = false
})
}

function initGenerate() {
activeModelId.value = props.modelId
activeModelId.value = props.modelId ?? ''
modelParams.value = cloneDeep(props.modelParamsSetting ?? {})
loadModels()
}

function resetData() {
activeModelId.value = ''
modelParams.value = {}
}

function requestGeneratePrompt(messages: PromptGenerateMessage[]) {
return store.postPromptGenerate(activeModelId.value, {
messages,
prompt: PROMPT_TEMPLATE,
model_params_setting: props.modelParamsSetting,
model_params_setting: modelParams.value,
})
}
</script>

<template>
<GenerateContent
v-model:model-id="activeModelId"
v-model:model-params="modelParams"
title="生成提示词"
placeholder="请输入提示词"
placeholder="请输入提示词主题"
empty-text="提示词显示在这里"
:disabled="!modelId"
:request="requestGeneratePrompt"
:model-options="modelOptions"
:provider-options="providerOptions"
:model-loading="modelLoading"
:model-params-loading="modelParamsLoading"
:generate-disabled="!activeModelId || modelLoading || modelParamsLoading"
@open="initGenerate"
@closed="resetData"
@replace="emit('replace', $event)"
Expand Down
Loading
Loading