qwen-code/packages/web-shell/client/components/panels/TodoPanel.tsx
ytahdn 48e5d5d0d7
feat(web-shell): polish stats table layout and todo panel UI (#6559)
* feat(web-shell): polish stats table layout and todo panel UI

- Use CSS grid for model usage table with fixed column widths
- Add loading spinner for in_progress todo items
- Add strikethrough for completed todo items
- Introduce nested variant for PivotRow to show thoughts as output sub-item
- Clarify i18n labels: stats.prompt -> Input Tokens, stats.output -> Output Tokens

* fix(web-shell): address review suggestions for stats table and todo panel

- Add prefers-reduced-motion media query for todo spinner (accessibility)
- Right-align numeric columns in model usage table for magnitude comparison
- Consolidate duplicate i18n keys (stats.prompt/output → stats.inputTokens/outputTokens)

* fix(web-shell): address stats review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-09 06:06:51 +00:00

89 lines
2.5 KiB
TypeScript

import { memo } from 'react';
import type { CSSProperties } from 'react';
import type { TodoItem } from '../../adapters/types';
import { getTodoStatusIcon } from '../../utils/todos';
import { useI18n } from '../../i18n';
import styles from './TodoPanel.module.css';
interface TodoPanelProps {
todos: TodoItem[];
title?: string;
}
function getStatusClass(status: TodoItem['status']): string {
switch (status) {
case 'completed':
return styles.completed;
case 'in_progress':
return styles.inProgress;
case 'pending':
return styles.pending;
}
}
export const TodoPanel = memo(function TodoPanel({
todos,
title,
}: TodoPanelProps) {
const { t } = useI18n();
if (todos.length === 0) return null;
const total = todos.length;
const inProgressIdx = todos.findIndex((td) => td.status === 'in_progress');
const currentIdx =
inProgressIdx >= 0
? inProgressIdx
: todos.findIndex((td) => td.status === 'pending');
const stepIndex = currentIdx >= 0 ? currentIdx + 1 : total;
const progress = total > 0 ? stepIndex / total : 0;
return (
<section
className={styles.panel}
aria-label={title ?? t('todo.title')}
tabIndex={0}
>
<div
className={styles.summary}
aria-label={t('todo.stepProgress', {
current: stepIndex,
total,
})}
>
<span
className={styles.progressRing}
style={{ '--todo-progress': String(progress) } as CSSProperties}
aria-hidden="true"
/>
<span className={styles.stepText}>
<span className={styles.fullText}>
{t('todo.stepProgress', { current: stepIndex, total })}
</span>
<span className={styles.compactText}>
{t('todo.stepFraction', { current: stepIndex, total })}
</span>
</span>
</div>
<div className={styles.detail} role="tooltip">
{todos.map((todo, index) => (
<div
key={`${todo.id || index}:${todo.content}`}
className={`${styles.item} ${getStatusClass(todo.status)}`}
>
<span className={styles.icon} aria-hidden="true">
{todo.status === 'in_progress' ? (
<span className={styles.loadingIcon} />
) : (
getTodoStatusIcon(todo.status)
)}
</span>
<span className={styles.content} title={todo.content}>
{todo.content}
</span>
</div>
))}
</div>
</section>
);
});