UI 修复: 回复页(ReplyBar)加往返预算配置入口

jianf 反馈:回复页面/对话页面/发件页面无法配置对话预算。
- 发件页(ComposePage):已有 maxRounds 输入框 
- 对话页(MailView 会话头部):已有 BudgetEditor 
- 回复页(ReplyBar):缺失 ← 本轮补上

ReplyBar 底部按钮区加预算徽标(点击展开编辑):
- 显示 已用/上限 来回 或「预算不限」,用尽时红色
- 点击 → 数字输入 + 保存/取消
- 复用 useSessionStore.setBudget(同一会话)
- 177 测试全过,已部署
This commit is contained in:
2026-09-07 18:15:26 +08:00
parent 83d3c2a51a
commit fd9f99a3f9

View File

@ -676,6 +676,9 @@ function ReplyBar({
// 抄送默认收起:多数回复不需要它,常驻一行输入框只会挤掉正文空间。
// 原邮件带抄送时自动展开并预填 —— 「回复全部」是人在这种场景下的默认预期
const [ccOpen, setCcOpen] = useState(false);
const [budgetEditing, setBudgetEditing] = useState(false);
const [budgetBusy, setBudgetBusy] = useState(false);
const [maxRoundsDraft, setMaxRoundsDraft] = useState('');
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -684,6 +687,8 @@ function ReplyBar({
const fetchSessions = useSessionStore(s => s.fetchSessions);
const fetchContacts = useContactStore(s => s.fetchContacts);
const selectSession = useSessionStore(s => s.selectSession);
const budget = useSessionStore(s => s.budget);
const setBudget = useSessionStore(s => s.setBudget);
const me = useAuthStore(s => s.user?.username || '');
if (!replyTo) return null;
@ -774,6 +779,58 @@ function ReplyBar({
<div className="flex items-center gap-2 mt-2 flex-wrap">
{error && <span className="text-xs text-red-600 min-w-0 break-words">{error}</span>}
<div className="flex-1" />
{budget && !budgetEditing && (
<button
onClick={() => {
setMaxRoundsDraft(budget.unlimited ? '' : String(budget.max_rounds));
setBudgetEditing(true);
}}
title="本任务往返预算Agent 主动发信上限(自动转发与权限询问不占)"
className={`tap inline-flex items-center gap-1 text-[10px] px-2 py-1 rounded border ${
budget.unlimited
? 'border-gray-200 text-gray-500 hover:border-blue-300'
: budget.remaining === 0
? 'border-red-200 text-red-600'
: 'border-gray-200 text-gray-500 hover:border-blue-300'
}`}
>
<GaugeIcon className="w-3 h-3" />
{budget.unlimited
? '预算不限'
: `${budget.used_rounds}/${budget.max_rounds} 来回${budget.remaining === 0 ? ' · 已用尽' : ''}`}
</button>
)}
{budget && budgetEditing && (
<span className="inline-flex items-center gap-1">
<span className="text-[10px] text-gray-500"></span>
<input
value={maxRoundsDraft}
onChange={e => setMaxRoundsDraft(e.target.value)}
inputMode="numeric"
placeholder="不限"
autoFocus
className="w-14 text-xs border border-gray-300 rounded px-1.5 py-0.5 focus:outline-none focus:ring-2 focus:ring-blue-100"
/>
<button
disabled={budgetBusy || (maxRoundsDraft.trim() !== '' && !/^\d+$/.test(maxRoundsDraft.trim()))}
onClick={async () => {
setBudgetBusy(true);
await setBudget({ max_rounds: maxRoundsDraft.trim() === '' ? 0 : Number(maxRoundsDraft.trim()) });
setBudgetBusy(false);
setBudgetEditing(false);
}}
className="tap px-2 py-0.5 text-[10px] rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40"
>
</button>
<button
onClick={() => setBudgetEditing(false)}
className="tap text-[10px] text-gray-400 hover:text-gray-700"
>
×
</button>
</span>
)}
<button
onClick={() => {
setBody('');