diff --git a/web/src/api/normalizers.test.mjs b/web/src/api/normalizers.test.mjs index 6293228..50cc0f6 100644 --- a/web/src/api/normalizers.test.mjs +++ b/web/src/api/normalizers.test.mjs @@ -590,8 +590,10 @@ test('project and app views keep workflow inside the selected project view', asy const thisFile = fileURLToPath(import.meta.url) const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`) const appPath = new URL('../App.vue', `file://${thisFile}`) + const stylePath = new URL('../styles.css', `file://${thisFile}`) const viewSource = await readFile(viewPath, 'utf8') const appSource = await readFile(appPath, 'utf8') + const styleSource = await readFile(stylePath, 'utf8') assert.doesNotMatch(viewSource, /sampleProjects/) assert.doesNotMatch(viewSource, /sampleAgentMatrix/) @@ -599,10 +601,18 @@ test('project and app views keep workflow inside the selected project view', asy assert.doesNotMatch(appSource, /工作流视图/) assert.match(viewSource, /selectedAgentId/) assert.match(viewSource, /selectAgent/) + assert.match(viewSource, /selectedWorkflowBatchName/) + assert.match(viewSource, /selectedWorkflowBatch/) + assert.match(viewSource, /selectWorkflowBatch/) + assert.match(viewSource, /批次切换/) + assert.match(viewSource, /role="group"/) + assert.match(viewSource, /selectedWorkflowBatch\?\.handoffCount \?\? 0/) assert.match(viewSource, /workflowBatches/) assert.match(viewSource, /工作流批次/) assert.match(viewSource, /supervision/) assert.match(viewSource, /goalSource/) + assert.match(styleSource, /\.batch-tab:hover/) + assert.match(styleSource, /\.batch-tab:focus-visible/) }) test('agent collaboration guide requires workflow batch metadata', async () => { diff --git a/web/src/styles.css b/web/src/styles.css index 1c52402..51dba47 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -267,6 +267,52 @@ button { margin-top: 18px; } +.batch-switcher { + grid-column: 1 / -1; + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; +} + +.batch-tab { + flex: 0 0 auto; + min-width: 190px; + max-width: 280px; + padding: 10px 12px; + color: var(--green); + text-align: left; + background: var(--panel-muted); + border: 1px solid var(--line); + border-radius: 8px; +} + +.batch-tab:hover { + border-color: var(--green); + box-shadow: 0 8px 20px rgb(36 139 107 / 12%); +} + +.batch-tab strong, +.batch-tab span { + display: block; +} + +.batch-tab span { + margin-top: 4px; + color: var(--muted); + font-size: 0.82rem; +} + +.batch-tab.active { + color: var(--panel); + background: var(--green); + border-color: var(--green); +} + +.batch-tab.active span { + color: color-mix(in srgb, var(--panel) 78%, white); +} + .flow-section { min-width: 0; padding-top: 16px; @@ -453,6 +499,7 @@ button { .project-item[role="button"]:focus-visible, .matrix-row[tabindex]:focus-visible, .agent-list-item:focus-visible, +.batch-tab:focus-visible, .tab-button:focus-visible { outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent); outline-offset: 2px; diff --git a/web/src/views/ProjectView.vue b/web/src/views/ProjectView.vue index 835fd45..4f82603 100644 --- a/web/src/views/ProjectView.vue +++ b/web/src/views/ProjectView.vue @@ -11,12 +11,16 @@ const projectState = ref(normalizeProjects()) const runtimeState = ref(normalizeRuntime()) const selectedProjectId = ref('') const selectedAgentId = ref('') +const selectedWorkflowBatchName = ref('') const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value)) const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || '')) const selectedAgent = computed(() => projectRuntime.value.agents.find((agent) => agent.id === selectedAgentId.value) ?? projectRuntime.value.agents[0], ) +const selectedWorkflowBatch = computed(() => + projectRuntime.value.workflowBatches.find((batch) => batch.name === selectedWorkflowBatchName.value) ?? projectRuntime.value.workflowBatches[0], +) onMounted(loadReadonlyData) @@ -29,15 +33,27 @@ watch( }, ) +watch( + () => projectRuntime.value.workflowBatches.map((batch) => batch.name).join('|'), + () => { + selectedWorkflowBatchName.value = projectRuntime.value.workflowBatches[0]?.name ?? '' + }, +) + function selectProject(projectId) { selectedProjectId.value = projectId selectedAgentId.value = '' + selectedWorkflowBatchName.value = '' } function selectAgent(agent) { selectedAgentId.value = agent.id } +function selectWorkflowBatch(batch) { + selectedWorkflowBatchName.value = batch.name +} + async function loadReadonlyData() { loading.value = true error.value = '' @@ -50,6 +66,7 @@ async function loadReadonlyData() { runtimeState.value = normalizeRuntime(runtimePayload) selectedProjectId.value = projectState.value.projects[0]?.id ?? '' selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? '' + selectedWorkflowBatchName.value = projectRuntime.value.workflowBatches[0]?.name ?? '' } catch (err) { error.value = err?.message || '连接后端接口失败' } finally { @@ -159,6 +176,21 @@ async function loadReadonlyData() {
+
+ +
+
@@ -167,17 +199,17 @@ async function loadReadonlyData() {
{{ projectRuntime.workflowBatches.length }} 个批次
-
-
+
+
- {{ batch.name }} - {{ batch.phaseCount }} 个阶段 · {{ batch.agentCount }} 个智能体 · {{ batch.handoffCount }} 条交接 + {{ selectedWorkflowBatch.name }} + {{ selectedWorkflowBatch.phaseCount }} 个阶段 · {{ selectedWorkflowBatch.agentCount }} 个智能体 · {{ selectedWorkflowBatch.handoffCount }} 条交接
- +
    -
  1. +
  2. {{ phase.name }} @@ -201,15 +233,15 @@ async function loadReadonlyData() {

    交互方向

    项目内智能体流程记录

    - {{ projectRuntime.handoffs.length }} 条交接 + {{ selectedWorkflowBatch?.handoffCount ?? 0 }} 条交接
-
-
+
+
- {{ batch.name }} - + {{ selectedWorkflowBatch.name }} +
-
+
{{ phase.name }}

这个阶段暂时没有交接边。