首页工作流:统一执行开关至发送按钮,最后一张已执行卡片参数可编辑
- 工作流表单卡片移除执行/重跑按钮,执行统一由 InputBar 发送按钮触发 - 会话执行过工作流即为工作流对话,下方输入框永久禁止文本输入 - 执行中发送按钮变停止,复用 teardownRound 按 runTypeMap 区分协议 - 最后一张已执行(done/failed)卡片参数保持可编辑,改参后经发送按钮重新执行(含必填校验) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,10 @@
|
||||
<!-- 工作流表单卡片:不套气泡,独立卡片渲染 -->
|
||||
<WorkflowFormCard
|
||||
v-if="msg.type === 'form'"
|
||||
:execute-signal="executeRequest?.msgId === msg.id ? executeRequest : null"
|
||||
:detail="msg.form"
|
||||
:readonly="msg.formStatus === 'done' || msg.formStatus === 'failed'"
|
||||
:editable="lastFormMsg ? msg.id === lastFormMsg.id : false"
|
||||
:submitting="msg.formStatus === 'running'"
|
||||
:form-error="msg.formError"
|
||||
:progress="msg.formProgress"
|
||||
@@ -95,7 +97,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Delete, DocumentCopy, RefreshRight } from '@element-plus/icons-vue';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
@@ -148,16 +150,25 @@ interface Props {
|
||||
loadingMore?: boolean;
|
||||
// 产出相对路径 → 完整 URL 解析函数(由首页传入 buildAssetUrl),内嵌预览媒体需完整地址
|
||||
resolveUrl?: (url: string) => string;
|
||||
// InputBar 发送按钮触发执行:父组件定位目标卡片后,经此信号调用卡片内部 submit/retry
|
||||
// (卡片参数在子组件内部,父组件无法直接取值,故由卡片暴露方法、这里中转调用)
|
||||
executeRequest?: { msgId: string; action: 'submit' | 'retry'; nonce: number } | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hasMore: false,
|
||||
loadingMore: false,
|
||||
resolveUrl: undefined,
|
||||
executeRequest: null,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
const chatListRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 会话中最后一张已执行的工作流卡片(done/failed):参数保持可编辑,便于改参后经发送按钮重新执行
|
||||
const lastFormMsg = computed(() =>
|
||||
[...props.messages].reverse().find((m) => m.type === 'form' && !!m.formStatus && m.formStatus !== 'editing')
|
||||
);
|
||||
|
||||
const isErrorMsg = (msg: ChatMessage): boolean => !msg.isUser && String(msg.content || '').startsWith('❌');
|
||||
|
||||
// 思考区折叠:未手动操作时,流式中展开、完成(出现答案)后自动收起(DeepSeek 形态)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
v-model="message"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 1, maxRows: 6 }"
|
||||
:disabled="generating || isWorkflowRunning || selectedWorkflowId !== null"
|
||||
:disabled="generating || isWorkflowRunning || selectedWorkflowId !== null || isWorkflowSession"
|
||||
:placeholder="inputPlaceholder"
|
||||
class="message-input"
|
||||
@focus="isFocused = true"
|
||||
@@ -50,11 +50,11 @@
|
||||
<span class="hint-text">Shift+Enter 换行</span>
|
||||
<button
|
||||
class="send-btn"
|
||||
:class="{ 'is-stop': generating && !isWorkflowRunning }"
|
||||
:disabled="selectedWorkflowId !== null || isWorkflowRunning || (!generating && (sendDisabled || !message.trim()))"
|
||||
@click="generating && !isWorkflowRunning ? emit('stop') : handleSend()"
|
||||
:class="{ 'is-stop': isStopping }"
|
||||
:disabled="sendBtnDisabled"
|
||||
@click="handleSend"
|
||||
>
|
||||
<el-icon v-if="generating && !isWorkflowRunning"><VideoPause /></el-icon>
|
||||
<el-icon v-if="isStopping"><VideoPause /></el-icon>
|
||||
<el-icon v-else><Top /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -92,12 +92,16 @@ interface Props {
|
||||
generating?: boolean;
|
||||
// 当前会话模型名称,用于输入栏切换入口展示
|
||||
currentModelName?: string;
|
||||
// 工作流正在执行中:输入框/发送按钮整体禁用,停止按钮转移到表单卡片 footer
|
||||
// 工作流正在执行中:输入框禁用,发送按钮变停止(与普通对话生成共用同一开关)
|
||||
isWorkflowRunning?: boolean;
|
||||
// 本会话为工作流会话(已执行过工作流):文本输入永久禁止,发送按钮仅执行工作流
|
||||
isWorkflowSession?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'send', message: string): void;
|
||||
// 工作流场景点击发送:父组件定位目标工作流卡片执行(编辑态提交 / 定格态重跑)
|
||||
(e: 'execute-workflow'): void;
|
||||
(e: 'workflow-select', workflowId: string | null, isTemplate?: boolean): void;
|
||||
(e: 'stop'): void;
|
||||
(e: 'select-model'): void;
|
||||
@@ -117,6 +121,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
generating: false,
|
||||
currentModelName: '',
|
||||
isWorkflowRunning: false,
|
||||
isWorkflowSession: false,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
const message = ref('');
|
||||
@@ -133,15 +138,26 @@ const visibleWorkflows = computed(() => commonWorkflows.value);
|
||||
// 输入框整体锁定:正处于生成/工作流执行中(执行中禁止切换工作流)
|
||||
const lockAll = computed(() => props.isWorkflowRunning || props.generating);
|
||||
|
||||
// 输入框占位符:工作流执行中给出明确提示
|
||||
// 输入框占位符:工作流执行中 / 选中工作流待填 / 工作流会话永久禁输入时给出明确提示
|
||||
const inputPlaceholder = computed(() =>
|
||||
props.isWorkflowRunning
|
||||
? '工作流执行中,请稍候...'
|
||||
: selectedWorkflowId.value !== null
|
||||
? '请填写上方工作流表单,提交后开始执行'
|
||||
: '有什么想聊的?按 Enter 发送,Shift+Enter 换行',
|
||||
: props.isWorkflowSession
|
||||
? '本会话为工作流对话,仅可执行工作流'
|
||||
: '有什么想聊的?按 Enter 发送,Shift+Enter 换行',
|
||||
);
|
||||
|
||||
// 生成中 / 工作流执行中:发送按钮变「停止」(普通对话与工作流共用同一开关)
|
||||
const isStopping = computed(() => props.generating || props.isWorkflowRunning);
|
||||
// 发送按钮可用性:执行中可点(停止);工作流场景可点(执行);普通对话需有文本且未禁用
|
||||
const sendBtnDisabled = computed(() => {
|
||||
if (props.generating || props.isWorkflowRunning) return false;
|
||||
if (selectedWorkflowId.value !== null || props.isWorkflowSession) return false;
|
||||
return props.sendDisabled || !message.value.trim();
|
||||
});
|
||||
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const res = await getWorkflowList();
|
||||
@@ -164,10 +180,19 @@ const fetchWorkflows = async () => {
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (props.sendDisabled) return;
|
||||
// 生成中 / 工作流执行中:发送按钮即「停止」(普通对话与工作流共用同一开关)
|
||||
if (props.generating || props.isWorkflowRunning) {
|
||||
emit('stop');
|
||||
return;
|
||||
}
|
||||
// 工作流场景(已选工作流 / 本会话为工作流会话):发送按钮即「执行工作流」
|
||||
if (selectedWorkflowId.value !== null || props.isWorkflowSession) {
|
||||
emit('execute-workflow');
|
||||
return;
|
||||
}
|
||||
// 普通对话:发送文本
|
||||
const msg = message.value.trim();
|
||||
// 空消息不发送(避免产生空用户气泡与空问题请求)
|
||||
if (!msg) return;
|
||||
if (!msg || props.sendDisabled) return;
|
||||
emit('send', msg);
|
||||
message.value = '';
|
||||
// 不清除工作流选择,保持表单可见
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:messages="messages"
|
||||
:has-more="hasMore"
|
||||
:loading-more="loadingMore"
|
||||
:execute-request="executeRequest"
|
||||
class="chat-list-scroll"
|
||||
@retry="emit('retry', $event)"
|
||||
@regenerate="emit('regenerate', $event)"
|
||||
@@ -64,6 +65,8 @@ interface Props {
|
||||
results?: VOSessionInfoResult[];
|
||||
hasMore?: boolean;
|
||||
loadingMore?: boolean;
|
||||
// InputBar 发送按钮触发执行信号(透传给 ChatList,定位工作流卡片执行)
|
||||
executeRequest?: { msgId: string; action: 'submit' | 'retry'; nonce: number } | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -87,6 +90,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
results: () => [],
|
||||
hasMore: false,
|
||||
loadingMore: false,
|
||||
executeRequest: null,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="header-right">
|
||||
<el-tag v-if="submitting" size="small" type="primary">执行中</el-tag>
|
||||
<el-tag v-else-if="isFailed" size="small" type="danger">执行失败</el-tag>
|
||||
<el-tag v-else-if="readonly" size="small" type="success">执行完成</el-tag>
|
||||
<el-tag v-else-if="formStatus === 'done'" size="small" type="success">执行完成</el-tag>
|
||||
<!-- 折叠切换:明确按钮提示用户可展开/收起已填参数(点击标题行同样可切换) -->
|
||||
<button type="button" class="collapse-toggle" @click.stop="toggleCollapse">
|
||||
<el-icon class="collapse-arrow" :class="{ collapsed }"><ArrowDown /></el-icon>
|
||||
@@ -163,29 +163,21 @@
|
||||
<el-empty v-if="!hasFormFields && !currentWorkflowHasPatchLayout" description="该工作流无需填写参数" :image-size="60" />
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区:editing 提交 / running 执行中 / done·failed 定格状态 -->
|
||||
<!-- 底部操作区:editing 提示 / running 执行中 / done·failed 状态标签(执行统一由 InputBar 发送按钮触发) -->
|
||||
<div class="workflow-form-footer">
|
||||
<div v-if="isFailed" class="form-error-text">{{ formError }}</div>
|
||||
<!-- 执行中:节点文本进度推进(后端 node_start/node_complete 事件),无进度时兜底「执行中...」 -->
|
||||
<!-- 取消执行:工作流执行中的终止入口(InputBar 停止按钮对工作流禁用,改由卡片 footer 提供) -->
|
||||
<!-- 执行中:节点文本进度推进(后端 node_start/node_complete 事件),无进度时兜底「执行中...」;
|
||||
停止统一由 InputBar 发送按钮(变停止)触发,卡片内不再提供取消入口 -->
|
||||
<div v-if="submitting" class="executing-progress">
|
||||
<span class="exec-spinner" />
|
||||
<span class="exec-text">{{ progressText }}</span>
|
||||
<el-button size="small" class="exec-cancel-btn" @click="emit('cancel')">取消执行</el-button>
|
||||
</div>
|
||||
<el-button v-else-if="!readonly" type="primary" :disabled="submitting" @click="handleSubmit">提交执行</el-button>
|
||||
<template v-else>
|
||||
<el-tag :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<!-- 失败卡片:可直接「重新执行」(复用当前已填值重跑),或「重新编辑」修改参数后再提交 -->
|
||||
<template v-if="isFailed">
|
||||
<el-button size="small" type="primary" @click="handleRetry">
|
||||
<el-icon><RefreshRight /></el-icon>重新执行
|
||||
</el-button>
|
||||
<el-button size="small" @click="emit('re-edit')">重新编辑</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 已执行卡片(done/failed,含可编辑的最后一张):显示状态标签,参数是否可编辑由 editable 决定 -->
|
||||
<el-tag v-else-if="formStatus === 'done' || formStatus === 'failed'" :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<!-- 编辑态:无执行按钮,提示通过下方发送按钮执行 -->
|
||||
<span v-else class="form-edit-tip">填写参数后,点击下方发送按钮执行工作流</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -193,7 +185,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ArrowDown, CircleCheckFilled, CircleCloseFilled, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
|
||||
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
import { collectHomeFormFields } from '../utils/flowDsl';
|
||||
@@ -210,6 +202,10 @@ interface Props {
|
||||
formError?: string;
|
||||
// 执行节点进度(node_start/node_complete 事件驱动的步骤列表,渲染执行过程区)
|
||||
progress?: { nodes: WorkflowNodeStep[] };
|
||||
// InputBar 发送按钮触发执行信号:仅当本卡片为目标时非 null(action 决定 submit/retry)
|
||||
executeSignal?: { action: 'submit' | 'retry'; nonce: number } | null;
|
||||
// 已执行卡片(done/failed)参数可编辑(会话中最后一张执行过的工作流):改参后经发送按钮重新执行
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -227,6 +223,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
submitting: false,
|
||||
formError: '',
|
||||
progress: () => ({ nodes: [] }),
|
||||
executeSignal: null,
|
||||
editable: false,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
@@ -237,9 +235,10 @@ const formFileNames = reactive<Record<string, string | string[]>>({});
|
||||
const uploadingFields = reactive<Record<string, boolean>>({});
|
||||
const templates = ref<any[]>([]);
|
||||
|
||||
// 状态派生:失败 = 携带 formError;禁用 = 定格或执行中
|
||||
// 状态派生:失败 = 携带 formError;禁用编辑 = 定格(done/failed)卡片默认只读,
|
||||
// 但最后一张已执行卡片(editable)允许改参;执行中(submitting)一律禁用
|
||||
const isFailed = computed(() => !!props.formError);
|
||||
const isDisabled = computed(() => props.readonly || props.submitting);
|
||||
const isDisabled = computed(() => (props.readonly && !props.editable) || props.submitting);
|
||||
|
||||
// 表单收起/展开:editing(待提交/回显)默认展开;提交(running)自动收起;
|
||||
// done/failed 保持提交时状态(默认收起,用户可手动展开查看已填值)
|
||||
@@ -256,7 +255,8 @@ watch(
|
||||
if (st === 'editing') {
|
||||
collapsed.value = false; // 回显/重新编辑:默认展开
|
||||
} else if (prev === undefined) {
|
||||
collapsed.value = true; // 初始即非编辑态(执行中/完成/失败)→ 收起
|
||||
// 初始即非编辑态(执行中/完成/失败):可编辑的最后一张已执行卡片展开供改参,其余收起
|
||||
collapsed.value = props.editable ? false : true;
|
||||
} else if (st === 'running') {
|
||||
collapsed.value = true; // 提交瞬间自动收起
|
||||
}
|
||||
@@ -541,7 +541,8 @@ watch(
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (props.readonly || props.submitting) return;
|
||||
// 定格卡片默认只读不可提交;最后一张已执行卡片(editable)允许改参后重新提交
|
||||
if ((props.readonly && !props.editable) || props.submitting) return;
|
||||
if (!validateFormFields()) return;
|
||||
emit('submit', {
|
||||
formValues: JSON.parse(JSON.stringify(formValues)),
|
||||
@@ -550,15 +551,27 @@ const handleSubmit = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 失败后直接重跑:复用当前已填值(必填此前已校验通过),不进入编辑态原样重新提交
|
||||
// 直接重跑:复用当前已填值(必填此前已校验通过),不进入编辑态原样重新提交。
|
||||
// 由 InputBar 发送按钮触发(未重新选择工作流时默认重跑最后一个),成功/失败定格卡片均可重跑
|
||||
const handleRetry = () => {
|
||||
if (!isFailed.value || props.submitting) return;
|
||||
if (!props.readonly || props.submitting) return;
|
||||
emit('retry', {
|
||||
formValues: JSON.parse(JSON.stringify(formValues)),
|
||||
formFileNames: JSON.parse(JSON.stringify(formFileNames)),
|
||||
templates: JSON.parse(JSON.stringify(templates.value || [])),
|
||||
});
|
||||
};
|
||||
|
||||
// InputBar 发送按钮触发执行:父组件把执行信号下发给目标卡片(本卡片为目标时 executeSignal 非 null),
|
||||
// 编辑态调用提交(含必填校验)、定格态调用重跑(复用已填值);卡片参数在组件内部,父组件经信号间接触发
|
||||
watch(
|
||||
() => props.executeSignal,
|
||||
(sig) => {
|
||||
if (!sig) return;
|
||||
if (sig.action === 'submit') handleSubmit();
|
||||
else handleRetry();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -772,6 +785,11 @@ const handleRetry = () => {
|
||||
white-space: pre-wrap; /* 失败详情可能含换行(error 字段多行原因),允许换行展示 */
|
||||
word-break: break-word;
|
||||
}
|
||||
.form-edit-tip {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 执行进度:spinner + 文本,节点事件推进 */
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
:results="currentSessionResults"
|
||||
:has-more="currentSessionHasMore"
|
||||
:loading-more="currentSessionLoadingMore"
|
||||
:execute-request="executeRequest"
|
||||
@retry="handleRetry"
|
||||
@regenerate="handleRetry"
|
||||
@delete="handleDeleteMessage"
|
||||
@@ -48,8 +49,10 @@
|
||||
:hide-shortcuts="isPlaceholder"
|
||||
:generating="isGenerating"
|
||||
:is-workflow-running="isWorkflowRunning"
|
||||
:is-workflow-session="isWorkflowSession"
|
||||
:current-model-name="currentChatModelName"
|
||||
@send="handleSend"
|
||||
@execute-workflow="handleExecuteWorkflow"
|
||||
@workflow-select="handleWorkflowSelect"
|
||||
@stop="handleStopGenerate"
|
||||
@select-model="sessionModelDialogVisible = true"
|
||||
@@ -438,8 +441,15 @@ const runTypeMap: Record<string, boolean> = {};
|
||||
|
||||
// 生成中:有活跃会话且该会话正处于发送状态(发送按钮切换为停止按钮)
|
||||
const isGenerating = computed(() => !!activeHistoryId.value && !!sendingSessions[activeHistoryId.value]);
|
||||
// 工作流执行中:isGenerating 且当前活跃会话本轮为工作流 → 输入框整体禁用,停止按钮转移到表单卡片 footer
|
||||
// 工作流执行中:isGenerating 且当前活跃会话本轮为工作流 → 输入框整体禁用,发送按钮变停止(统一开关)
|
||||
const isWorkflowRunning = computed(() => isGenerating.value && runTypeMap[activeHistoryId.value || ''] === true);
|
||||
// 会话是否为「工作流会话」:会话中已有执行过(非 editing)的工作流表单 → InputBar 永久禁止文本输入
|
||||
const isWorkflowSession = computed(() => {
|
||||
const sid = activeHistoryId.value;
|
||||
if (!sid) return false;
|
||||
const list = sessionMessages.value.get(sid);
|
||||
return !!list && list.some((m) => m.type === 'form' && !!m.formStatus && m.formStatus !== 'editing');
|
||||
});
|
||||
const inputBarRef = ref<any>(null);
|
||||
const templateDialogVisible = ref(false);
|
||||
const pendingTemplate = ref<any>(null);
|
||||
@@ -961,6 +971,29 @@ const handleStopGenerate = () => {
|
||||
teardownRound(sid);
|
||||
};
|
||||
|
||||
// ===== InputBar 发送按钮(工作流场景)执行 =====
|
||||
// 目标工作流卡片执行信号:下发给目标卡片(ChatList 经 executeRequest → executeSignal),卡片内部提交/重跑。
|
||||
// 卡片参数在组件内部,父组件经信号间接触发、不直接取值。
|
||||
const executeRequest = ref<{ msgId: string; action: 'submit' | 'retry'; nonce: number } | null>(null);
|
||||
// 1) 会话存在编辑态卡片(用户已选工作流在填参数)→ 提交该卡片;
|
||||
// 2) 否则未重新选择工作流 → 默认重跑最后一张已执行(done/failed)的卡片
|
||||
const handleExecuteWorkflow = () => {
|
||||
const sid = activeHistoryId.value;
|
||||
if (!sid || sendingSessions[sid]) return; // 执行中不重复触发(此时按钮为停止态,点击走 stop)
|
||||
const list = sessionMessages.value.get(sid) || [];
|
||||
const reversed = [...list].reverse();
|
||||
const editing = reversed.find((m) => m.type === 'form' && m.formStatus === 'editing');
|
||||
const lastExecuted = reversed.find((m) => m.type === 'form' && !!m.formStatus && m.formStatus !== 'editing');
|
||||
const target = editing || lastExecuted;
|
||||
if (!target) return;
|
||||
// 目标卡片均保持可编辑(editing 或最后一张已执行),统一走提交(含必填校验)
|
||||
executeRequest.value = {
|
||||
msgId: target.id,
|
||||
action: 'submit',
|
||||
nonce: (executeRequest.value?.nonce || 0) + 1,
|
||||
};
|
||||
};
|
||||
|
||||
// ===== 工作流执行:选中工作流 → 表单页 WS 执行 → 完成后切回对话页 =====
|
||||
|
||||
// 工作流节点进度推进:node_start → 该节点置「进行中」;node_complete → 置「完成」。
|
||||
|
||||
Reference in New Issue
Block a user