fix: clarify supervisor review and activity layouts
Work Review could place its empty state beside the controls, while redundant copy and secondary activity statistics obscured progress. Use a full-width vertical review flow with a separate run status, connected review stages, prominent saved-batch coverage, and expandable secondary details. Validation: 33 component tests, 40 Electron fixture layout cases, typecheck, targeted ESLint, and diff checks passed. No model calls. Release note: 修复工作回顾空态布局,精简顶部控件与重复说明。活动记录通过相连阶段和已保存批次展示进度,次要统计可在详情中展开。
This commit is contained in:
@@ -22,8 +22,7 @@ export function SupervisionReviewStages({ row }: { row: SupervisionActivity }) {
|
||||
</li>
|
||||
})}
|
||||
</ol>
|
||||
<p className="supervisor-activity__coverage">{t('reviewSettings.progress', { batches: progress.batches, characters: progress.characters, remaining: progress.remainingSources })}</p>
|
||||
{progress.navigationNodes !== undefined && <p className="supervisor-activity__note">{t('activity.navigationSaved', { count: progress.navigationNodes })}</p>}
|
||||
<p className="supervisor-activity__coverage">{t('activity.coverage', { batches: progress.batches, remaining: progress.remainingSources })}</p>
|
||||
{!progress.complete && !progress.phase && <p className="supervisor-activity__note">{t('activity.stageUnknown')}</p>}
|
||||
{row.supervisionStatus === 'running' && <p className="supervisor-activity__note">{t('reviewSettings.inFlight', { count: progress.inFlight ?? 0 })}</p>}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ it.each(['zh-CN', 'en-US'] as const)('retains operational failure details and re
|
||||
expect(details.closest('details')).toHaveAttribute('open')
|
||||
expect(details).toHaveStyle({ userSelect: 'text' })
|
||||
expect(screen.getByText(copy.activity.status.failed)).toBeVisible()
|
||||
expect(screen.getByText(copy.activity.description)).toBeVisible()
|
||||
expect(screen.queryByText(copy.activity.description)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: copy.reviewSettings.resume })).toBeEnabled()
|
||||
})
|
||||
|
||||
@@ -134,12 +134,17 @@ it.each(['summarizing', 'saving', undefined] as const)('renders saved phase %s w
|
||||
settings: { pageSize: 50, batchCharacters: 8000, batchMessages: 20, executionSeconds: 300, timeoutSeconds: 240, concurrency: 1 } }
|
||||
}]), resume } })
|
||||
const { container } = render(<SupervisorActivity active projects={[]} onOpenResult={vi.fn()} />)
|
||||
await screen.findByText(/已保存 6 批/)
|
||||
expect(await screen.findByText('已保存 6 批 · 剩余 0 个来源')).toBeVisible()
|
||||
const detailedProgress = screen.getByText('已保存 6 批 · 已读 4515 个字符 · 剩余 0 个来源')
|
||||
expect(detailedProgress).not.toBeVisible()
|
||||
expect(container.querySelector('[data-phase="saving"]')).toHaveAttribute('data-state', phase === 'saving' ? 'failed' : phase ? 'pending' : 'unknown')
|
||||
expect(container.querySelector('[data-phase="summarizing"]')).toHaveAttribute('data-state', phase === 'saving' ? 'completed' : phase ? 'failed' : 'unknown')
|
||||
expect(container.querySelector('.supervisor-activity__diagnostics')).not.toHaveAttribute('open')
|
||||
expect(container.querySelector('.supervisor-activity__advanced')).not.toHaveAttribute('open')
|
||||
expect(screen.getByText('本次回顾的已保存配置')).not.toBeVisible()
|
||||
fireEvent.click(container.querySelector('.supervisor-activity__advanced > summary')!)
|
||||
expect(detailedProgress).toBeVisible()
|
||||
expect(screen.getByText('本次回顾的已保存配置')).toBeVisible()
|
||||
const action = screen.getByRole('button', { name: phase === 'summarizing' ? '继续合并摘要' : phase === 'saving' ? '重试发布' : '继续已保存回顾' })
|
||||
expect(action.compareDocumentPosition(container.querySelector('.supervisor-activity__diagnostics')!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
fireEvent.click(action)
|
||||
|
||||
@@ -89,7 +89,6 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
|
||||
<RefreshCw size={14} aria-hidden="true" />{t('center.actions.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="supervisor-activity__note">{t('activity.description')}</p>
|
||||
{error && <div role="alert" className="heartbeat-center__error"><p>{error}</p>
|
||||
<button className="secondary-button" type="button" onClick={() => reload()}>{t('center.actions.retry')}</button>
|
||||
</div>}
|
||||
@@ -102,7 +101,6 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
|
||||
<time dateTime={row.startedAt} title={row.startedAt}>{date(row.startedAt)}</time>
|
||||
</header>
|
||||
<p className="supervisor-activity__scope">{!row.scope ? t('activity.unknownScope') : row.scope.kind === 'global' ? t('center.scope.global') : t('supervisor.projectScope', { names: row.scope.projectIds.map((id) => projects.find((project) => project.id === id)?.name ?? t('settings.scope.unavailableProject')).join(', ') })} · {t(`activity.triggers.${row.trigger}`)}</p>
|
||||
{row.reviewProgress && <SupervisionReviewStages row={row} />}
|
||||
{!row.reviewProgress && <>
|
||||
<p className="supervisor-activity__stages">
|
||||
{row.heartbeatStatus && <span>{t('activity.heartbeatStage')}: {t(`statuses.run.${row.heartbeatStatus}`)}</span>}
|
||||
@@ -123,6 +121,7 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
|
||||
<button type="button" className="secondary-button" aria-expanded={expanded === row.id} aria-controls={`batches-${row.id}`} onClick={() => setExpanded(expanded === row.id ? undefined : row.id)}>{t('reviewSettings.facts')}</button>
|
||||
</div>
|
||||
</>}
|
||||
{row.reviewProgress && <SupervisionReviewStages row={row} />}
|
||||
{row.error && <div className="supervisor-activity__failure">
|
||||
<p>{t(row.status === 'paused' ? 'reviewSettings.pausedHint' : /persistedId|candidateRef|entity identity/.test(row.error)
|
||||
? 'reviewSettings.identityError' : 'reviewSettings.runError')}</p>
|
||||
@@ -141,6 +140,8 @@ export function SupervisorActivity({ active, projects, onOpenResult, configId, c
|
||||
<div><dt>{t('activity.finished')}</dt><dd>{row.completedAt ? date(row.completedAt) : t('common.unavailable')}</dd></div>
|
||||
{row.timeRange && <div><dt>{t('supervisor.period')}</dt><dd>{date(row.timeRange.from)} / {date(row.timeRange.to)}</dd></div>}
|
||||
</dl>
|
||||
{row.reviewProgress && <p>{t('reviewSettings.progress', { batches: row.reviewProgress.batches, characters: row.reviewProgress.characters, remaining: row.reviewProgress.remainingSources })}</p>}
|
||||
{row.reviewProgress?.navigationNodes !== undefined && <p>{t('activity.navigationSaved', { count: row.reviewProgress.navigationNodes })}</p>}
|
||||
{row.reviewProgress && row.heartbeatStatus && <p>{t('activity.heartbeatStage')}: {t(`statuses.run.${row.heartbeatStatus}`)}</p>}
|
||||
{row.summary && <p className="supervisor-activity__summary">{row.summary}</p>}
|
||||
{row.reviewProgress?.settings && <details><summary>{t('reviewSettings.configuration')}</summary>
|
||||
|
||||
@@ -449,7 +449,7 @@ export function SupervisorWorkspace({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{(pending === 'run' || pausedReview) && <div className="supervisor-workspace__actions" role="status">
|
||||
{(pending === 'run' || pausedReview) && <div className="supervisor-workspace__run-status" role="status">
|
||||
<span>{t(pausedReview ? 'reviewSettings.pausedHint' : 'supervisor.runningHint')}</span>
|
||||
{onOpenActivity && <button className="link-button" onClick={onOpenActivity}>{t('activity.title')}</button>}
|
||||
</div>}
|
||||
@@ -480,7 +480,6 @@ export function SupervisorWorkspace({
|
||||
{tab === 'overview' && (
|
||||
<>
|
||||
<div className="supervisor-workspace__toolbar" role="group" aria-label={t('supervisor.newReview')}>
|
||||
<strong>{t('supervisor.newReview')}</strong>
|
||||
<label>
|
||||
{t('supervisor.scope')}
|
||||
<select
|
||||
@@ -529,9 +528,6 @@ export function SupervisorWorkspace({
|
||||
{t('center.actions.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="supervisor-workspace__muted">
|
||||
{t('supervisor.sourcesHint')}
|
||||
</p>
|
||||
{results.length > 0 && <div className="supervisor-workspace__result-navigation">
|
||||
<label>
|
||||
{t('supervisor.history')}
|
||||
|
||||
@@ -41,6 +41,7 @@ export const heartbeat = {
|
||||
phases: { collecting: 'Collect sources', extracting: 'Review batches', summarizing: 'Merge summaries', saving: 'Publish review' },
|
||||
stageStates: { completed: 'Complete', running: 'In progress', failed: 'Failed', paused: 'Paused', pending: 'Not started', unknown: 'Not recorded', skipped: 'Not needed', no_change: 'No changes' },
|
||||
stageUnknown: 'This historical run has no saved stage. Zero remaining sources does not mean the review was published.',
|
||||
coverage: '{{batches}} saved batches · {{remaining}} sources remaining',
|
||||
navigationSaved: '{{count}} navigation merges saved',
|
||||
resumeMerge: 'Continue merging', resumeSave: 'Retry publication', runDetails: 'Run details and configuration',
|
||||
treePageHint: 'Expand projects and conversations to browse this page of saved batches. Tasks without a saved result are not listed here.',
|
||||
|
||||
@@ -38,6 +38,7 @@ export const heartbeat = {
|
||||
phases: { collecting: '收集来源', extracting: '整理批次', summarizing: '合并摘要', saving: '发布回顾' },
|
||||
stageStates: { completed: '已完成', running: '进行中', failed: '失败', paused: '已暂停', pending: '未开始', unknown: '未记录', skipped: '无需执行', no_change: '无变化' },
|
||||
stageUnknown: '此历史运行未保存阶段信息。剩余来源为零不代表回顾已发布。',
|
||||
coverage: '已保存 {{batches}} 批 · 剩余 {{remaining}} 个来源',
|
||||
navigationSaved: '已保存 {{count}} 次导航合并',
|
||||
resumeMerge: '继续合并摘要', resumeSave: '重试发布', runDetails: '运行详情与配置',
|
||||
treePageHint: '按项目、会话展开本页已保存批次。这里不包含尚未保存结果的任务。',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.supervisor-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
container: supervisor / inline-size;
|
||||
@@ -7,6 +9,23 @@
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.supervisor-workspace > .empty-state {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-self: stretch;
|
||||
}
|
||||
.supervisor-workspace__run-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.supervisor-workspace p,
|
||||
.supervisor-workspace time {
|
||||
overflow-wrap: anywhere;
|
||||
@@ -607,9 +626,6 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.supervisor-workspace__toolbar > strong {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.supervisor-workspace__recap,
|
||||
.supervisor-workspace__detail,
|
||||
.supervisor-workspace__source,
|
||||
@@ -673,11 +689,16 @@
|
||||
.supervisor-activity__status--failed { color: var(--danger); }
|
||||
.supervisor-activity__item > header time { margin-left: auto; color: var(--text-secondary); font-size: var(--font-caption); }
|
||||
.supervisor-activity__scope { color: var(--text-secondary); margin-block: var(--space-2) var(--space-4); }
|
||||
.supervisor-activity__steps { display: flex; flex-wrap: wrap; gap: var(--space-4); padding: 0; margin: 0; list-style: none; }
|
||||
.supervisor-activity__steps li { display: flex; align-items: baseline; gap: var(--space-2); padding-block: var(--space-1); }
|
||||
.supervisor-activity__workflow { margin-top: var(--space-4); }
|
||||
.supervisor-activity__steps { display: flex; gap: var(--space-3); padding: 0; margin: 0; list-style: none; }
|
||||
.supervisor-activity__steps li { display: flex; flex: 1; min-width: 0; align-items: center; gap: var(--space-2); padding-block: var(--space-1); }
|
||||
.supervisor-activity__steps li:not(:last-child)::after { content: ''; flex: 1; min-width: var(--space-3); border-top: 1px solid var(--border-control); }
|
||||
.supervisor-activity__steps li[aria-current='step'] strong { color: var(--accent); font-weight: 600; }
|
||||
.supervisor-activity__steps strong { font-size: var(--font-body); font-weight: 500; }
|
||||
.supervisor-activity__steps small { margin-left: var(--space-2); color: var(--text-secondary); font-size: var(--font-caption); }
|
||||
.supervisor-activity__step-number { color: var(--text-muted); }
|
||||
.supervisor-activity__step-number { display: grid; place-items: center; flex: 0 0 24px; height: 24px; border: 1px solid var(--border-control); border-radius: 50%; color: var(--text-secondary); font-size: var(--font-caption); }
|
||||
.supervisor-activity__steps [aria-current='step'] .supervisor-activity__step-number { border-color: var(--accent); background: var(--accent-subtle); color: var(--accent); }
|
||||
.supervisor-activity__steps [data-state='completed'] .supervisor-activity__step-number { border-color: var(--success); color: var(--success); }
|
||||
.supervisor-activity__steps [data-state="failed"] small { color: var(--danger); }
|
||||
.supervisor-activity__steps [data-state="running"] small { color: var(--accent); }
|
||||
.supervisor-activity__steps [data-state="completed"] small { color: var(--success); }
|
||||
@@ -699,6 +720,7 @@
|
||||
.supervisor-activity__batch-label { display: inline-block; vertical-align: top; max-width: calc(100% - var(--space-4)); max-height: 3em; line-height: 1.5; overflow: hidden; }
|
||||
@media (max-width: 719px) {
|
||||
.supervisor-activity__steps { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
|
||||
.supervisor-activity__steps li:nth-child(2)::after { display: none; }
|
||||
.supervisor-activity__steps small { display: block; margin-left: 0; margin-top: var(--space-1); }
|
||||
.supervisor-activity__item > header time { margin-left: 0; flex-basis: 100%; }
|
||||
.supervisor-activity__conversation, .supervisor-activity__batch { margin-left: var(--space-1); padding-left: var(--space-2); }
|
||||
|
||||
Reference in New Issue
Block a user