From 611119dea0f5634fb2875632e29ddb33ca14f3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Wed, 26 Aug 2026 22:48:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E8=BF=90=E8=A1=8C=E8=BE=93=E5=85=A5=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并运行参数与问题输入并锁定首轮参数 - 优化文件上传、参数摘要与十二小时草稿恢复 - 补充输入表单国际化与相关测试 --- .../src/locales/langs/en-US/aiWorkflow.json | 1 + .../src/locales/langs/zh-CN/aiWorkflow.json | 1 + .../src/views/ai/resource/ChooseResource.vue | 15 +- .../workflow/components/WorkflowChatPage.vue | 849 +++++++++++++++--- .../workflow/components/WorkflowFileInput.vue | 274 ++++-- .../workflow/components/WorkflowFormItem.vue | 35 +- .../components/WorkflowImageInput.vue | 37 +- .../__tests__/workflowFileInput.test.ts | 58 ++ .../__tests__/workflowFormParameters.test.ts | 26 +- .../workflowFormPresentation.test.ts | 40 + .../__tests__/workflowRunDraft.test.ts | 104 +++ .../components/workflowFormParameters.ts | 25 + .../components/workflowFormPresentation.ts | 34 +- .../workflow/components/workflowRunDraft.ts | 173 ++++ 14 files changed, 1447 insertions(+), 225 deletions(-) create mode 100644 easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/workflowFileInput.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/workflowRunDraft.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/workflow/components/workflowRunDraft.ts diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json index 21284f08..561d45a2 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json @@ -22,6 +22,7 @@ "visibilityScopePublic": "Public", "visibilityScopePublicDesc": "Available to internal users matched by category", "params": "Params", + "runInputForm": "Input form", "steps": "Steps", "result": "Result", "confirm": "For contents to be confirmed, please confirm first!", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json index 62aacaee..520a6426 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json @@ -22,6 +22,7 @@ "visibilityScopePublic": "公开", "visibilityScopePublicDesc": "分类命中的内部用户可访问", "params": "执行参数", + "runInputForm": "输入表单", "steps": "执行步骤", "result": "执行结果", "confirm": "有待确认的内容,请先确认!", diff --git a/easyflow-ui-admin/app/src/views/ai/resource/ChooseResource.vue b/easyflow-ui-admin/app/src/views/ai/resource/ChooseResource.vue index d61670f0..cab4e84c 100644 --- a/easyflow-ui-admin/app/src/views/ai/resource/ChooseResource.vue +++ b/easyflow-ui-admin/app/src/views/ai/resource/ChooseResource.vue @@ -14,6 +14,10 @@ const props = defineProps({ type: String, required: true, }, + disabled: { + type: Boolean, + default: false, + }, multiple: { type: Boolean, default: false, @@ -37,13 +41,20 @@ const pageUrl = computed(() => { : `${baseUrl}?resourceType=${props.resourceType}`; }); function openDialog() { + if (props.disabled) { + return; + } dialogVisible.value = true; } function closeDialog() { dialogVisible.value = false; } function confirm() { - emit('choose', props.multiple ? chooseResources.value : currentChoose.value, props.attrName); + emit( + 'choose', + props.multiple ? chooseResources.value : currentChoose.value, + props.attrName, + ); closeDialog(); } watch( @@ -85,7 +96,7 @@ watch( - + {{ $t('button.choose') }} diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue index 60768bd6..bbee2085 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue @@ -12,18 +12,29 @@ import type { WorkflowExecutionStepView, } from './workflowExecutionDetails'; -import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'; +import { + computed, + nextTick, + onBeforeUnmount, + onMounted, + ref, + watch, +} from 'vue'; import { useRoute } from 'vue-router'; import { ChatTimeline, defaultAssistantAvatar } from '@easyflow/common-ui'; +import { useUserStore } from '@easyflow/stores'; import { + ArrowDown, ArrowLeft, + ArrowUp, Promotion, RefreshLeft, Share, Tickets, } from '@element-plus/icons-vue'; +import { useResizeObserver } from '@vueuse/core'; import { ElAvatar, ElButton, @@ -32,6 +43,7 @@ import { ElDrawer, ElEmpty, ElForm, + ElIcon, ElSkeleton, ElTag, ElTooltip, @@ -39,8 +51,10 @@ import { import { api, SseClient } from '#/api/request'; import workflowIcon from '#/assets/ai/workflow/workflowIcon.png'; +import { $t } from '#/locales'; import { router } from '#/router'; import { navigateBackToList } from '#/router/list-return-context'; +import { resolveAgentChatIdentity } from '#/utils/agent-chat-cache'; import { copyTextWithFeedback } from '#/utils/clipboard-feedback'; import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context'; import { resolveWorkflowShareFailureReason } from '#/utils/workflow-share-context'; @@ -55,10 +69,17 @@ import WorkflowFinalOutput from './WorkflowFinalOutput.vue'; import WorkflowFormItem from './WorkflowFormItem.vue'; import { resolveWorkflowFormParameters } from './workflowFormParameters'; import { + buildWorkflowFormParameterSummaries, buildWorkflowFormSubmissionImages, - buildWorkflowFormSubmissionText, - hasRequiredWorkflowFormParameters, } from './workflowFormPresentation'; +import { + buildWorkflowRunDraftKey, + getWorkflowRunDraftStorage, + hasWorkflowRunDraftContent, + readWorkflowRunDraft, + removeWorkflowRunDraft, + writeWorkflowRunDraft, +} from './workflowRunDraft'; import { formatWorkflowElapsed, formatWorkflowProgressLabel, @@ -82,7 +103,11 @@ const props = withDefaults( }, ); +const MAX_COLLAPSED_PARAMETER_COUNT = 4; +const PRIMARY_PARAMETER_COUNT = 4; +const DRAFT_SAVE_DELAY_MS = 300; const route = useRoute(); +const userStore = useUserStore(); const streamClient = new SseClient(); const loading = ref(true); const loadError = ref(''); @@ -94,10 +119,14 @@ const running = ref(false); const stopping = ref(false); const executeId = ref(''); const runStatusKey = ref(''); -const extraReady = ref(false); -const extraSubmitted = ref(false); -const extraSubmitting = ref(false); +const parametersLocked = ref(false); +const parametersExpanded = ref(false); +const moreParametersExpanded = ref(false); +const parametersValidating = ref(false); +const parameterChipsRef = ref(); +const parameterChipsWidth = ref(0); const extraValues = ref>({}); +const defaultExtraValues = ref>({}); const extraFormRef = ref(); const waitingConfirmation = ref>(); const confirmValues = ref>({}); @@ -120,6 +149,8 @@ const sharing = ref(false); const manualAbort = ref(false); const lastRunningNodeName = ref(''); let progressStatusTimer = 0; +let draftSaveTimer = 0; +let draftReady = false; let userMessageSequence = 0; const formParameters = computed(() => @@ -130,6 +161,61 @@ const additionalParameters = computed(() => (parameter: any) => parameter.name !== 'user_input', ), ); +const parameterSummaries = computed(() => + buildWorkflowFormParameterSummaries( + additionalParameters.value, + extraValues.value, + ), +); +const primaryParameters = computed(() => { + const parameters = additionalParameters.value; + if (parameters.length <= PRIMARY_PARAMETER_COUNT) { + return parameters; + } + + const requiredCount = parameters.filter( + (parameter: any) => parameter.required === true, + ).length; + let optionalSlots = Math.max(0, PRIMARY_PARAMETER_COUNT - requiredCount); + return parameters.filter((parameter: any) => { + if (parameter.required === true) { + return true; + } + if (optionalSlots <= 0) { + return false; + } + optionalSlots -= 1; + return true; + }); +}); +const moreParameters = computed(() => { + const primaryParameterSet = new Set(primaryParameters.value); + return additionalParameters.value.filter( + (parameter: any) => !primaryParameterSet.has(parameter), + ); +}); +const collapsedParameterLimit = computed(() => { + const width = parameterChipsWidth.value; + if (width <= 0 || width >= 560) { + return MAX_COLLAPSED_PARAMETER_COUNT; + } + if (width >= 400) { + return 3; + } + if (width >= 260) { + return 2; + } + return 1; +}); +const visibleParameterSummaries = computed(() => + parameterSummaries.value.slice(0, collapsedParameterLimit.value), +); +const hiddenParameterSummaryCount = computed(() => + Math.max( + 0, + parameterSummaries.value.length - visibleParameterSummaries.value.length, + ), +); const confirmParameters = computed(() => { const parameters = Array.isArray(waitingConfirmation.value?.parameters) ? waitingConfirmation.value?.parameters @@ -150,8 +236,7 @@ const confirmKey = computed(() => { const composerDisabled = computed( () => loading.value || - !extraReady.value || - extraSubmitting.value || + parametersValidating.value || running.value || Boolean(waitingConfirmation.value), ); @@ -202,15 +287,28 @@ const detailDurationText = computed(() => { return duration === undefined ? '—' : `${duration} ms`; }); -onMounted(loadPage); +useResizeObserver(parameterChipsRef, ([entry]) => { + parameterChipsWidth.value = entry?.contentRect.width || 0; +}); + +watch([question, extraValues], scheduleDraftSave, { deep: true }); + +onMounted(() => { + window.addEventListener('pagehide', persistDraft); + void loadPage(); +}); onBeforeUnmount(() => { + window.removeEventListener('pagehide', persistDraft); + clearDraftSaveTimer(); + persistDraft(); manualAbort.value = true; streamClient.abort(); clearProgressStatusTimer(); }); async function loadPage() { + draftReady = false; loading.value = true; loadError.value = ''; try { @@ -225,6 +323,9 @@ async function loadPage() { }); descriptor.value = response.data || {}; initializeAdditionalValues(); + restoreDraft(); + await nextTick(); + draftReady = true; } catch (error: any) { loadError.value = error?.message || '工作流加载失败'; } finally { @@ -251,7 +352,14 @@ function initializeAdditionalValues() { const values: Record = {}; for (const parameter of additionalParameters.value) { const defaultValue = parameter.defaultValue; - if (Array.isArray(defaultValue)) { + if ( + parameter.contentType === 'file' && + (defaultValue === null || + defaultValue === undefined || + defaultValue === '') + ) { + values[parameter.name] = []; + } else if (Array.isArray(defaultValue)) { values[parameter.name] = [...defaultValue]; } else if (defaultValue && typeof defaultValue === 'object') { values[parameter.name] = { ...defaultValue }; @@ -259,12 +367,88 @@ function initializeAdditionalValues() { values[parameter.name] = defaultValue ?? ''; } } + defaultExtraValues.value = values; extraValues.value = values; - extraSubmitted.value = additionalParameters.value.length === 0; - extraReady.value = !hasRequiredWorkflowFormParameters( + parametersLocked.value = false; + parametersExpanded.value = buildWorkflowFormParameterSummaries( + additionalParameters.value, + values, + ).some((summary) => summary.required && !summary.ready); + moreParametersExpanded.value = false; + parametersValidating.value = false; +} + +function draftKey() { + if (!workflowId.value) { + return ''; + } + return buildWorkflowRunDraftKey( + workflowId.value, + resolveAgentChatIdentity(userStore.userInfo), + props.shareMode, + ); +} + +function restoreDraft() { + const draft = readWorkflowRunDraft( + getWorkflowRunDraftStorage(), + draftKey(), additionalParameters.value, ); - extraSubmitting.value = false; + if (!draft) { + return false; + } + question.value = draft.question; + extraValues.value = { ...extraValues.value, ...draft.values }; + parametersExpanded.value = buildWorkflowFormParameterSummaries( + additionalParameters.value, + extraValues.value, + ).some((summary) => summary.required && !summary.ready); + return hasWorkflowRunDraftContent( + question.value, + extraValues.value, + defaultExtraValues.value, + ); +} + +function clearDraftSaveTimer() { + if (draftSaveTimer) { + window.clearTimeout(draftSaveTimer); + draftSaveTimer = 0; + } +} + +function scheduleDraftSave() { + if (!draftReady) { + return; + } + clearDraftSaveTimer(); + draftSaveTimer = window.setTimeout(() => { + draftSaveTimer = 0; + persistDraft(); + }, DRAFT_SAVE_DELAY_MS); +} + +function persistDraft() { + if (!draftReady) { + return; + } + const storage = getWorkflowRunDraftStorage(); + const key = draftKey(); + if ( + !hasWorkflowRunDraftContent( + question.value, + extraValues.value, + defaultExtraValues.value, + ) + ) { + removeWorkflowRunDraft(storage, key); + return; + } + writeWorkflowRunDraft(storage, key, { + question: question.value, + values: extraValues.value, + }); } async function backToWorkflowList() { @@ -276,42 +460,26 @@ async function backToWorkflowList() { ); } -async function submitAdditionalInputs() { - if (extraSubmitted.value) { +async function validateAdditionalInputs() { + if (parametersLocked.value) { return true; } - if (extraSubmitting.value) { + if (additionalParameters.value.length === 0) { + return true; + } + if (parametersValidating.value) { return false; } - extraSubmitting.value = true; + parametersExpanded.value = true; + await nextTick(); + parametersValidating.value = true; try { - if (!(await extraFormRef.value?.validate().catch(() => false))) { - return false; - } - extraReady.value = true; - extraSubmitted.value = true; - const content = buildWorkflowFormSubmissionText( - additionalParameters.value, - extraValues.value, - ); - const images = buildWorkflowFormSubmissionImages( - additionalParameters.value, - extraValues.value, - ); - if (content || images.length > 0) { - appendUserMessage(content, images); - } - await nextTick(); - return true; + return (await extraFormRef.value?.validate().catch(() => false)) ?? true; } finally { - extraSubmitting.value = false; + parametersValidating.value = false; } } -async function confirmAdditionalInputs() { - await submitAdditionalInputs(); -} - function appendStatus( label: string, status: 'done' | 'running', @@ -442,11 +610,17 @@ async function handleSend() { if (!content || composerDisabled.value || !workflowId.value) { return; } - if (!extraSubmitted.value && !(await submitAdditionalInputs())) { + if (!(await validateAdditionalInputs())) { return; } - appendUserMessage(content); + const images = buildWorkflowFormSubmissionImages( + additionalParameters.value, + extraValues.value, + ); + parametersLocked.value = true; + appendUserMessage(content, images); question.value = ''; + parametersExpanded.value = false; running.value = true; executionState.value = 'running'; executionStartedAt.value = Date.now(); @@ -725,6 +899,9 @@ async function resetConversation() { return; } } + draftReady = false; + clearDraftSaveTimer(); + removeWorkflowRunDraft(getWorkflowRunDraftStorage(), draftKey()); manualAbort.value = true; streamClient.abort(); timelineItems.value = []; @@ -745,6 +922,8 @@ async function resetConversation() { confirmError.value = ''; lastRunningNodeName.value = ''; initializeAdditionalValues(); + await nextTick(); + draftReady = true; } async function loadExecutionDetail() { @@ -956,40 +1135,6 @@ function executionTraceText( -
-
-

{{ descriptor.startFormMeta?.title || '补充信息' }}

-

- {{ descriptor.startFormMeta.description }} -

-
- - - -
- - {{ descriptor.startFormMeta?.submitText || '继续' }} - -
-
-
-
- - - -
+
+
+
+
+ {{ $t('aiWorkflow.runInputForm') }} +
+ +
+ + +
+ + +
+ + +
+

+ {{ descriptor.startFormMeta.description }} +

+ +
+
+ +
+ +
+ + + +
+ +
+
+
+
+
+
+
+
+ +
+ + + +
+
Enter 发送 · Shift + Enter 换行
@@ -1293,7 +1570,7 @@ function executionTraceText( .workflow-chat__timeline { width: min(920px, 100%); - padding: 24px 24px 136px; + padding: 24px 24px 152px; margin: 0 auto; } @@ -1370,9 +1647,9 @@ function executionTraceText( z-index: 2; display: flex; flex-direction: column; - gap: 6px; + gap: var(--space-2); align-items: center; - padding: 16px 24px; + padding: var(--space-5) var(--space-6); pointer-events: none; background: linear-gradient( to bottom, @@ -1383,32 +1660,276 @@ function executionTraceText( .workflow-chat__input-shell { display: flex; - gap: 8px; - align-items: flex-end; - width: min(880px, 100%); - padding: 10px 10px 10px 16px; + flex-direction: column; + gap: var(--space-2); + width: min(920px, 100%); pointer-events: auto; - background: var(--el-bg-color); - border: 1px solid var(--el-border-color-lighter); - border-radius: 20px; - box-shadow: var(--el-box-shadow-lighter); - transition: - border-color 160ms ease, - box-shadow 160ms ease; } -.workflow-chat__input-shell:focus-within { +.workflow-chat__parameter-panel, +.workflow-chat__input-row { + background: var(--el-bg-color); + background: color-mix(in srgb, var(--el-bg-color) 96%, transparent); + border: 1px solid var(--el-border-color-lighter); + backdrop-filter: blur(18px); + transition: + border-color var(--motion-duration-base) var(--motion-ease-standard), + box-shadow var(--motion-duration-base) var(--motion-ease-standard); +} + +.workflow-chat__parameter-panel { + overflow: hidden; + border-radius: var(--radius-panel); + box-shadow: var(--shadow-subtle); +} + +.workflow-chat__input-row { + overflow: hidden; + border-radius: var(--radius-panel); + box-shadow: var(--shadow-toolbar); +} + +.workflow-chat__input-row:focus-within { border-color: var(--el-color-primary-light-5); box-shadow: 0 0 0 3px var(--el-color-primary-light-9), - var(--el-box-shadow-lighter); + var(--shadow-toolbar); } -.workflow-chat__input-shell textarea { +.workflow-chat__parameter-summary { + display: flex; + gap: var(--space-3); + align-items: center; + min-height: 48px; + padding: var(--space-2) var(--space-4); + border-bottom: 1px solid var(--el-border-color-extra-light); +} + +.workflow-chat__parameter-panel:not(.is-expanded) + .workflow-chat__parameter-summary { + border-bottom-color: transparent; +} + +.workflow-chat__parameter-heading { + display: flex; + flex: 0 0 auto; + gap: var(--space-2); + align-items: center; + min-width: 0; + font-size: 13px; + font-weight: 600; + color: var(--el-text-color-primary); +} + +.workflow-chat__parameter-chips { + display: flex; flex: 1; - min-height: 38px; + gap: var(--space-2); + min-width: 0; + overflow: hidden; +} + +.workflow-chat__parameter-chip { + display: inline-flex; + flex: 1 1 0; + gap: var(--space-2); + align-items: center; + min-width: 0; + max-width: 240px; + min-height: 36px; + padding: 0 var(--space-3); + font: inherit; + font-size: 12px; + color: var(--el-text-color-secondary); + cursor: pointer; + background: hsl(var(--surface-subtle)); + border: 1px solid var(--el-border-color-light); + border-radius: var(--radius-control); + transition: + color var(--motion-duration-fast) var(--motion-ease-standard), + background-color var(--motion-duration-fast) var(--motion-ease-standard), + border-color var(--motion-duration-fast) var(--motion-ease-standard); +} + +.workflow-chat__parameter-chip.is-overflow { + flex: 0 0 auto; + min-width: 42px; + color: hsl(var(--primary)); +} + +.workflow-chat__parameter-chip:hover, +.workflow-chat__parameter-chip:focus-visible { + color: hsl(var(--primary)); + outline: none; + background: hsl(var(--primary) / 6%); + border-color: hsl(var(--primary) / 28%); +} + +.workflow-chat__parameter-chip:focus-visible { + box-shadow: 0 0 0 3px hsl(var(--primary) / 10%); +} + +.workflow-chat__parameter-chip.is-pending strong { + color: var(--el-color-warning); +} + +.workflow-chat__parameter-chip span, +.workflow-chat__parameter-chip strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workflow-chat__parameter-chip span { + flex: 0 1 auto; +} + +.workflow-chat__parameter-chip strong { + flex: 1 1 auto; + font-weight: 500; + color: var(--el-text-color-primary); +} + +.workflow-chat__parameter-toggle { + display: inline-flex; + flex: 0 0 auto; + gap: var(--space-1); + align-items: center; + min-height: 36px; + padding: 0 var(--space-2); + margin-left: auto; + font: inherit; + font-size: 13px; + font-weight: 500; + color: hsl(var(--primary)); + cursor: pointer; + background: transparent; + border: 0; + border-radius: var(--radius-control); +} + +.workflow-chat__parameter-toggle:hover, +.workflow-chat__parameter-toggle:focus-visible { + outline: none; + background: hsl(var(--primary) / 7%); +} + +.workflow-chat__parameter-toggle:focus-visible { + box-shadow: 0 0 0 3px hsl(var(--primary) / 10%); +} + +.workflow-chat__parameter-editor { + max-height: min(46vh, 420px); + padding: var(--space-3) var(--space-4) var(--space-4); + overflow: auto; + overscroll-behavior: contain; +} + +.workflow-chat__parameter-description { + margin: 0 0 var(--space-3); + font-size: 12px; + line-height: 20px; + color: var(--el-text-color-secondary); +} + +.workflow-chat__parameter-form fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +.workflow-chat__parameter-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-3) var(--space-4); +} + +.workflow-chat__parameter-grid :deep(.workflow-form-item) { + min-width: 0; + margin-bottom: 0; +} + +.workflow-chat__parameter-grid :deep(.workflow-form-item.is-wide) { + grid-column: 1 / -1; +} + +.workflow-chat__parameter-grid :deep(.el-form-item__label) { + padding-bottom: var(--space-2); + font-size: 12px; + font-weight: 500; + line-height: 18px; + color: var(--el-text-color-secondary); +} + +.workflow-chat__parameter-grid :deep(.el-input__wrapper), +.workflow-chat__parameter-grid :deep(.el-select__wrapper), +.workflow-chat__parameter-grid :deep(.el-textarea__inner) { + border-radius: var(--radius-control); +} + +.workflow-chat__more-parameters { + padding-top: var(--space-2); + margin-top: var(--space-3); + border-top: 1px solid var(--el-border-color-extra-light); +} + +.workflow-chat__more-parameters-toggle { + display: inline-flex; + gap: var(--space-2); + align-items: center; + min-height: 36px; + padding: 0 var(--space-2); + font: inherit; + font-size: 12px; + font-weight: 500; + color: var(--el-text-color-secondary); + cursor: pointer; + background: transparent; + border: 0; + border-radius: var(--radius-control); +} + +.workflow-chat__more-parameters-toggle small { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 var(--space-1); + font-size: 11px; + color: hsl(var(--primary)); + background: hsl(var(--primary) / 8%); + border-radius: 999px; +} + +.workflow-chat__more-parameters-toggle:hover, +.workflow-chat__more-parameters-toggle:focus-visible { + color: hsl(var(--primary)); + outline: none; + background: hsl(var(--primary) / 6%); +} + +.workflow-chat__more-parameters-toggle:focus-visible { + box-shadow: 0 0 0 3px hsl(var(--primary) / 10%); +} + +.workflow-chat__more-parameters-grid { + margin-top: var(--space-3); +} + +.workflow-chat__input-row { + display: flex; + gap: var(--space-3); + align-items: flex-end; + padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4); +} + +.workflow-chat__input-row textarea { + flex: 1; + min-height: 48px; max-height: 120px; - padding: 8px 0; + padding: var(--space-2) 0; overflow: auto; font: inherit; line-height: 22px; @@ -1419,18 +1940,19 @@ function executionTraceText( border: 0; } -.workflow-chat__input-shell textarea::placeholder { +.workflow-chat__input-row textarea::placeholder { color: var(--el-text-color-placeholder); } -.workflow-chat__input-shell textarea:disabled { +.workflow-chat__input-row textarea:disabled { cursor: not-allowed; } .workflow-chat__send { flex: 0 0 auto; - width: 38px; - height: 38px; + width: 40px; + min-width: 40px; + height: 40px; } .workflow-chat__send.is-stop::before { @@ -1447,6 +1969,35 @@ function executionTraceText( color: var(--el-text-color-placeholder); } +.workflow-parameters-enter-active, +.workflow-parameters-leave-active { + overflow: hidden; + transition: + max-height var(--motion-duration-medium) var(--motion-ease-decelerate), + opacity var(--motion-duration-base) var(--motion-ease-standard), + transform var(--motion-duration-medium) var(--motion-ease-decelerate); +} + +.workflow-parameters-enter-from, +.workflow-parameters-leave-to { + max-height: 0; + opacity: 0; + transform: translateY(8px); +} + +.workflow-parameter-more-enter-active, +.workflow-parameter-more-leave-active { + transition: + opacity var(--motion-duration-base) var(--motion-ease-standard), + transform var(--motion-duration-base) var(--motion-ease-standard); +} + +.workflow-parameter-more-enter-from, +.workflow-parameter-more-leave-to { + opacity: 0; + transform: translateY(-4px); +} + .workflow-chat__detail { min-height: 240px; } @@ -1702,13 +2253,47 @@ function executionTraceText( } .workflow-chat__timeline { - padding: 16px 16px 128px; + padding: 16px 16px 168px; } .workflow-chat__composer { padding: 12px 16px; } + .workflow-chat__parameter-summary { + flex-wrap: wrap; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + } + + .workflow-chat__parameter-heading { + flex: 1; + } + + .workflow-chat__parameter-chips { + flex-basis: calc(100% - 72px); + order: 3; + } + + .workflow-chat__parameter-toggle { + margin-left: auto; + } + + .workflow-chat__parameter-editor { + max-height: min(46vh, 440px); + padding: var(--space-3); + } + + .workflow-chat__parameter-grid { + grid-template-columns: 1fr; + gap: var(--space-3); + } + + .workflow-chat__input-row { + gap: var(--space-2); + padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3); + } + .workflow-chat__form-card { width: calc(100% - 32px); padding: 20px; @@ -1721,7 +2306,11 @@ function executionTraceText( } .workflow-chat__step-chevron, - .workflow-chat__step-title { + .workflow-chat__step-title, + .workflow-parameter-more-enter-active, + .workflow-parameter-more-leave-active, + .workflow-parameters-enter-active, + .workflow-parameters-leave-active { transition: none; } } diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFileInput.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFileInput.vue index d5614718..b4e14472 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFileInput.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFileInput.vue @@ -1,15 +1,19 @@ @@ -161,16 +207,83 @@ function clearFiles() { display: flex; flex-direction: column; gap: 10px; + width: 100%; } .workflow-file-input__native { display: none; } -.workflow-file-input__hint { - font-size: 12px; +.workflow-file-input__dropzone { + display: flex; + align-items: center; + width: 100%; + min-height: 56px; color: var(--el-text-color-secondary); - line-height: 1.5; + background: hsl(var(--surface-subtle)); + border: 1px dashed var(--el-border-color); + border-radius: var(--radius-control); + transition: + color var(--motion-duration-base) var(--motion-ease-standard), + background-color var(--motion-duration-base) var(--motion-ease-standard), + border-color var(--motion-duration-base) var(--motion-ease-standard); +} + +.workflow-file-input__dropzone:hover, +.workflow-file-input__dropzone.is-dragging { + color: hsl(var(--primary)); + background: hsl(var(--primary) / 6%); + border-color: hsl(var(--primary) / 48%); +} + +.workflow-file-input__dropzone.is-disabled, +.workflow-file-input__dropzone.is-disabled:hover { + color: var(--el-text-color-placeholder); + background: var(--el-fill-color-light); + border-color: var(--el-border-color-lighter); +} + +.workflow-file-input__upload-trigger { + display: inline-flex; + flex: 1; + gap: var(--space-2); + align-items: center; + align-self: stretch; + min-width: 0; + padding: var(--space-2) var(--space-3); + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + background: transparent; + border: 0; +} + +.workflow-file-input__upload-trigger:focus-visible { + outline: none; + box-shadow: 0 0 0 3px hsl(var(--primary) / 12%); +} + +.workflow-file-input__upload-trigger:disabled { + cursor: default; + opacity: 0.72; +} + +.workflow-file-input__upload-icon { + font-size: 18px; +} + +.workflow-file-input__dropzone-copy { + display: flex; + flex-flow: row wrap; + gap: var(--space-1) var(--space-2); + align-items: center; + line-height: 1.4; +} + +.workflow-file-input__dropzone-copy small { + font-size: 11px; + color: var(--el-text-color-placeholder); } .workflow-file-input__list { @@ -181,39 +294,46 @@ function clearFiles() { .workflow-file-input__summary { display: flex; + gap: 12px; align-items: center; justify-content: space-between; - gap: 12px; - padding: 12px; - border: 1px solid var(--el-border-color-light); - border-radius: 10px; + padding: var(--space-3); background: var(--el-fill-color-blank); + border: 1px solid var(--el-border-color-light); + border-radius: var(--radius-control); +} + +.workflow-file-input__file-icon { + flex: 0 0 auto; + font-size: 18px; + color: hsl(var(--primary)); } .workflow-file-input__content { - min-width: 0; flex: 1; + min-width: 0; } .workflow-file-input__name { font-size: 13px; font-weight: 600; color: var(--el-text-color-primary); - word-break: break-word; + overflow-wrap: anywhere; } .workflow-file-input__meta { display: flex; - align-items: center; gap: 10px; + align-items: center; margin-top: 6px; font-size: 12px; color: var(--el-text-color-secondary); } -.workflow-file-input__actions { - display: flex; - flex-wrap: wrap; - gap: 8px; +.workflow-file-input__ready { + display: inline-flex; + gap: var(--space-1); + align-items: center; + color: var(--el-color-success); } diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue index 4915538c..e5f4758b 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFormItem.vue @@ -17,6 +17,10 @@ import { resolveWorkflowParameterLabel } from './workflowFormParameters'; import { hasWorkflowImageValue } from './workflowImageValue'; const props = defineProps({ + disabled: { + type: Boolean, + default: false, + }, parameters: { type: Array, required: true, @@ -46,6 +50,10 @@ function isResource(contentType: any) { function isFileContentType(contentType: any) { return contentType === 'file'; } +function isWideItem(item: any) { + const contentType = getContentType(item); + return item.formType === 'textarea' || contentType === 'image'; +} function getCheckboxOptions(item: any) { if (item.enums) { return ( @@ -74,7 +82,9 @@ function buildRules(item: any) { return; } if (Array.isArray(value)) { - callback(value.length > 0 ? undefined : new Error($t('message.required'))); + callback( + value.length > 0 ? undefined : new Error($t('message.required')), + ); return; } if (value && typeof value === 'object') { @@ -91,10 +101,16 @@ function buildRules(item: any) { ]; } function updateParam(name: string, value: any) { + if (props.disabled) { + return; + } const newValue = { ...props.runParams, [name]: value }; emit('update:runParams', newValue); } function choose(data: any, propName: string) { + if (props.disabled) { + return; + } updateParam(propName, data.resourceUrl); } @@ -102,6 +118,8 @@ function choose(data: any, propName: string) {