feat(webui): 真正动可见层的现代化 —— 字号、层次、间距、分隔线
# 起因:上一轮的「现代化」基本不算现代化
用户指出「我说的是 webui 现代化」。回看上一轮,我交付的其实是**底层改进**:
圆角加大一档、自定义滚动条、焦点环、过渡、reduce-motion、令牌与可访问性。
这些都对,但**可见变化几乎只有圆角** —— 界面看起来还是老样子。
实测数据确认了「老」在哪:
text-xs(12px) 172 处 ← 被当正文用
text-[10px] 84 处
text-[11px] 68 处
text-[9px] 19 处 ← 现代显示器上基本读不了
text-sm(14px) 69 处
text-base(16px) 4 处
57 处 border-b + 21 处 border-r,其中 67 条是 border-gray-200 的硬灰线
阴影:全站共 10 处,且全是 Tailwind 系统默认档;
index.css 里 --shadow-1/2/3 三个语义令牌**定义了但零处使用**
所以真正的病因是三条:**字太小、层次为零、硬线切分**。
# 改动
## 1. 字号体系抬一档(tailwind.config.js)
不用 Tailwind 默认档,重定为:
3xs 11px(角标下限,取代 9/10px 魔法数字)
2xs 12px(元信息,取代 11px)
xs 13px(次要正文,原 12px —— 拿它当正文的地方自动变舒适)
sm 14px(正文)
base 15px
并把 171 处裸 px 类名(text-[9px]/[10px]/[11px])统一换成令牌 ——
顺带消除魔法数字。行高一起给:小档位 1.35/1.45,正文 1.55,
只放大字号不放行高会把密排列表顶得很难看。
## 2. 把层次接出来(原本是死代码)
tailwind.config.js 新增 boxShadow 映射 `--shadow-1/2/3` + 新增
`--shadow-panel`(横向偏移 + 大扩散,竖向几乎不偏移,否则全高面板像浮在半空)。
用于:列表面板(lg:shadow-panel,**同时去掉 border-r 硬线**)、
登录/初始化卡片(shadow-sm → shadow-2 + 去硬边框)、
地址自动补全下拉(shadow-lg → shadow-2)、窄屏滑入详情面板
(shadow-2xl → shadow-3 + 去 border-l)、主题分段控件的选中滑块。
深色下层次比浅色更难感知,所以 --shadow-panel 在深色里更实一些;
深色里靠边框分组几乎看不见,层次是**唯一**有效的分组手段。
## 3. 分隔线软化(改令牌而不是改 67 处类名)
`--c-gray-200` 浅色 229 231 235 → 234 236 241,深色 44 49 59 → 39 43 52。
改在令牌上,67 条边框 + 8 处底色一次性生效且不会漏。
**刻意没有一起调 gray-300**:它同时是滚动条滑块色,调淡会让滑块更难看见。
## 4. 配比放宽(列表行的呼吸感)
MailList:行内距 px-3 py-2.5 → px-3.5 py-3,列表 gap space-y-0.5 → space-y-1,
表头 py-3 → py-3.5。未读主题字重 medium → semibold,已读 gray-500 → gray-600。
## 5. 量出来的两个真实对比度缺陷(不是估算)
新增 `test/manual/modernization-verify.mjs`,用真实渲染做四条判据。
它量出浅色下两个 WCAG AA 不达标(阈值 4.5:1):
- 会话别名 `text-blue-500` 白底 3.68:1(别名在 mail list / thread / mailview
共 4 处,都是 11px 小字)→ 改 blue-600/700,达 5.17:1
- 时间戳 `text-gray-400` 压在选中行淡蓝底 `bg-blue-50` 上 4.44:1
第二个的**根因是调色板缺一档**:浅色下 `--c-gray-400` 与 `--c-gray-500`
完全相同(都是 107 114 128),于是「比次要文字再深一档的中间色」根本不存在,
时间戳无处可退。拉开 gray-500 → 90 98 112(5.65:1),并把 5 个列表组件的
行内元信息(19 处)从 gray-400 提到 gray-500。
# 验证
- typecheck 干净
- 前端全量 `npm test` EXIT=0(markdown-xss / narrow-layout / theme 30 /
background 15 / vitest 216)
- **真实渲染** `modernization-verify.mjs`:浅色 8/8、深色 8/8,判据含
最小字号 ≥ 11px(改造前 9px)、邮件正文 ≥ 14px、列表面板真有 box-shadow、
gray-200 是软化值、40 处正文对比度全部达标
# 我自己的三处错(都被这次的度量拦下)
1. **判据量错对象**:第一版拿「收件箱列表」要求 40% 元素 ≥13px,量出 39.7%
判失败 —— 而收件箱本质是元信息密集区,发件人/时间/别名本来就该小。
改成量真正该达标的**邮件正文**(≥14px)。
2. **探针忽略 alpha**:`parseRgb` 把 `rgba(239,246,255,0.4)` 的 alpha 丢掉当实色,
于是把淡蓝底当纯蓝算出 4.44:1 的假缺陷。改为按画家算法合成整条背景链。
3. **config 注释换算写错**:3xs 注释写 10px,0.6875rem 其实是 11px。
This commit is contained in:
@ -145,7 +145,7 @@ export default function AccountPage() {
|
||||
</h3>
|
||||
<form onSubmit={changePw} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">当前密码</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">当前密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={oldPw}
|
||||
@ -155,7 +155,7 @@ export default function AccountPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">新密码(至少 8 位)</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">新密码(至少 8 位)</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPw}
|
||||
@ -165,7 +165,7 @@ export default function AccountPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">确认新密码</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">确认新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPw}
|
||||
@ -175,7 +175,7 @@ export default function AccountPage() {
|
||||
mismatch ? 'border-red-300' : 'border-gray-300 focus:border-blue-400'
|
||||
}`}
|
||||
/>
|
||||
{mismatch && <p className="mt-1 text-[10px] text-red-500">两次密码不一致</p>}
|
||||
{mismatch && <p className="mt-1 text-3xs text-red-500">两次密码不一致</p>}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
@ -181,12 +181,12 @@ export default function AddressInput({
|
||||
|
||||
{open && items.length > 0 && (
|
||||
<div
|
||||
className={`absolute z-20 w-full overflow-y-auto bg-white border border-gray-200 rounded-md shadow-lg ${
|
||||
className={`absolute z-20 w-full overflow-y-auto bg-white border border-gray-200 rounded-lg shadow-2 ${
|
||||
menuLayout.flip ? 'bottom-full mb-1' : 'top-full mt-1'
|
||||
}`}
|
||||
style={{ maxHeight: menuLayout.maxHeight }}
|
||||
>
|
||||
<div className="px-2.5 py-1 text-[10px] text-gray-400 border-b border-gray-100">
|
||||
<div className="px-2.5 py-1 text-3xs text-gray-400 border-b border-gray-100">
|
||||
{hint}
|
||||
</div>
|
||||
{items.map((s, i) => {
|
||||
@ -216,23 +216,23 @@ export default function AddressInput({
|
||||
一条已经在跑的会话,而不是继续一条已有的邮件往来 */}
|
||||
{c?.source === 'platform' && (
|
||||
<span
|
||||
className="shrink-0 px-1 py-0.5 rounded bg-blue-100 text-blue-700 text-[9px]"
|
||||
className="shrink-0 px-1 py-0.5 rounded bg-blue-100 text-blue-700 text-3xs"
|
||||
title="平台侧已有的会话,本站还没有对应的邮件往来"
|
||||
>
|
||||
平台
|
||||
</span>
|
||||
)}
|
||||
{c?.source === 'new' && (
|
||||
<span className="shrink-0 text-[10px] text-gray-400 font-sans">新建会话</span>
|
||||
<span className="shrink-0 text-3xs text-gray-400 font-sans">新建会话</span>
|
||||
)}
|
||||
{(c?.unread ?? 0) > 0 && (
|
||||
<span className="shrink-0 px-1 py-0.5 rounded bg-red-600 text-white text-[9px]">
|
||||
<span className="shrink-0 px-1 py-0.5 rounded bg-red-600 text-white text-3xs">
|
||||
{c!.unread}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{c?.title && c.source !== 'new' && (
|
||||
<p className="text-[10px] text-gray-400 truncate mt-0.5">{c.title}</p>
|
||||
<p className="text-3xs text-gray-400 truncate mt-0.5">{c.title}</p>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
@ -194,18 +194,18 @@ function UserCard({ user, scopes, expanded, onToggle, onSaved, onReload, setErro
|
||||
<ChevronRightIcon className={`w-3 h-3 transition-transform ${expanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
<span className="font-mono text-sm text-gray-900 min-w-[100px]">{user.username}</span>
|
||||
<span className="tap text-[11px] text-gray-500">{user.display_name}</span>
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded ${user.role === 'admin' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'}`}>
|
||||
<span className="tap text-2xs text-gray-500">{user.display_name}</span>
|
||||
<span className={`text-3xs px-1.5 py-0.5 rounded ${user.role === 'admin' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{user.role === 'admin' ? '管理员' : '用户'}
|
||||
</span>
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded ${user.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-200 text-gray-500'}`}>
|
||||
<span className={`text-3xs px-1.5 py-0.5 rounded ${user.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-200 text-gray-500'}`}>
|
||||
{user.status === 'active' ? '启用' : '禁用'}
|
||||
</span>
|
||||
{user.role !== 'admin' && (user.allowed_agents.length > 0 || user.allowed_paths.length > 0) && (
|
||||
<span className="text-[10px] text-gray-400">受限</span>
|
||||
<span className="text-3xs text-gray-400">受限</span>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<span className="text-[10px] text-gray-400">{user.last_login || '从未登录'}</span>
|
||||
<span className="text-3xs text-gray-400">{user.last_login || '从未登录'}</span>
|
||||
</div>
|
||||
{expanded && <UserEditor user={user} scopes={scopes} onSaved={onSaved} onReload={onReload} setError={setError} />}
|
||||
</div>
|
||||
@ -258,12 +258,12 @@ function UserEditor({ user, scopes, onSaved, onReload, setError }: {
|
||||
<div className="border-t border-gray-100 bg-gray-50 px-4 py-3 space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">显示名</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">显示名</label>
|
||||
<input value={displayName} onChange={e => setDisplayName(e.target.value)}
|
||||
className="w-full text-sm border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">角色</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">角色</label>
|
||||
<select value={role} onChange={e => setRole(e.target.value as 'admin' | 'user')}
|
||||
className="w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400">
|
||||
<option value="user">普通用户</option>
|
||||
@ -271,7 +271,7 @@ function UserEditor({ user, scopes, onSaved, onReload, setError }: {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">状态</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">状态</label>
|
||||
{user.status === 'active' ? (
|
||||
<button onClick={disableUser} className="px-3 py-1.5 text-xs rounded-md border border-red-300 text-red-600 hover:bg-red-50 w-full">禁用</button>
|
||||
) : (
|
||||
@ -283,7 +283,7 @@ function UserEditor({ user, scopes, onSaved, onReload, setError }: {
|
||||
{user.role !== 'admin' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1.5">
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1.5">
|
||||
可调用 Agent {agents.length > 0 && <span className="text-gray-400">({agents.length} 项)</span>}
|
||||
<span className="ml-2 font-normal text-gray-400">未勾选 = 不限</span>
|
||||
</label>
|
||||
@ -297,7 +297,7 @@ function UserEditor({ user, scopes, onSaved, onReload, setError }: {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1.5">
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1.5">
|
||||
可访问目录 {paths.length > 0 && <span className="text-gray-400">({paths.length} 项)</span>}
|
||||
<span className="ml-2 font-normal text-gray-400">未勾选 = 不限;按目录前缀匹配</span>
|
||||
</label>
|
||||
@ -322,7 +322,7 @@ function UserEditor({ user, scopes, onSaved, onReload, setError }: {
|
||||
<input type="password" value={pw} onChange={e => setPw(e.target.value)} placeholder="新密码(至少 8 位)"
|
||||
className="w-40 text-xs border border-gray-300 rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
|
||||
<button onClick={resetPassword} disabled={pw.length < 8 || busy}
|
||||
className="tap inline-flex items-center gap-1 px-2 py-1 text-[11px] rounded bg-chrome-700 text-white hover:bg-chrome-800 disabled:opacity-40">
|
||||
className="tap inline-flex items-center gap-1 px-2 py-1 text-2xs rounded bg-chrome-700 text-white hover:bg-chrome-800 disabled:opacity-40">
|
||||
<CheckIcon className="w-3 h-3" /> 重置
|
||||
</button>
|
||||
</div>
|
||||
@ -402,7 +402,7 @@ function ScopePick({ label, items, selected, onToggle, color }: {
|
||||
const active = color === 'blue' ? 'bg-blue-50 border-blue-300 text-blue-700' : 'bg-green-50 border-green-300 text-green-700';
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">
|
||||
{label} <span className="font-normal text-gray-400">未勾选 = 不限</span>
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@ -421,8 +421,8 @@ function Field({ label, hint, children }: { label: string; hint?: string; childr
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline gap-1.5 mb-1">
|
||||
<label className="text-[11px] font-medium text-gray-500">{label}</label>
|
||||
{hint && <span className="text-[10px] text-gray-400">{hint}</span>}
|
||||
<label className="text-2xs font-medium text-gray-500">{label}</label>
|
||||
{hint && <span className="text-3xs text-gray-400">{hint}</span>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -11,7 +11,7 @@ export function AttachmentList({ items }: { items: Attachment[] }) {
|
||||
<div className="mt-4 border-t border-gray-100 pt-3">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<PaperclipIcon className="w-3.5 h-3.5 text-gray-400" />
|
||||
<span className="text-[11px] font-medium text-gray-500">
|
||||
<span className="text-2xs font-medium text-gray-500">
|
||||
附件 {items.length}
|
||||
</span>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@ export function AttachmentList({ items }: { items: Attachment[] }) {
|
||||
>
|
||||
<FileIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
||||
<span className="text-xs text-gray-800 truncate flex-1">{a.filename}</span>
|
||||
<span className="text-[10px] text-gray-400 shrink-0">
|
||||
<span className="text-3xs text-gray-400 shrink-0">
|
||||
{api.formatSize(a.size_bytes)}
|
||||
</span>
|
||||
<DownloadIcon className="w-3.5 h-3.5 text-gray-300 group-hover:text-blue-500 shrink-0" />
|
||||
@ -126,7 +126,7 @@ export function AttachmentPicker({
|
||||
</button>
|
||||
|
||||
{uploading && (
|
||||
<span className="min-w-0 flex-1 inline-flex items-center gap-1.5 text-[11px] text-gray-500">
|
||||
<span className="min-w-0 flex-1 inline-flex items-center gap-1.5 text-2xs text-gray-500">
|
||||
<SpinnerIcon className="w-3.5 h-3.5 animate-spin shrink-0" />
|
||||
<span className="truncate">{uploading.name}</span>
|
||||
<span className="shrink-0">{uploading.pct}%</span>
|
||||
@ -134,14 +134,14 @@ export function AttachmentPicker({
|
||||
)}
|
||||
|
||||
{items.length > 0 && !uploading && (
|
||||
<span className="text-[11px] text-gray-500 min-w-0 break-words">
|
||||
<span className="text-2xs text-gray-500 min-w-0 break-words">
|
||||
{items.length} 个附件 ·{' '}
|
||||
{api.formatSize(items.reduce((sum, a) => sum + a.size, 0))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="text-[11px] text-red-600 break-words">{error}</div>}
|
||||
{error && <div className="text-2xs text-red-600 break-words">{error}</div>}
|
||||
|
||||
{items.length > 0 && (
|
||||
<ul className="space-y-1">
|
||||
@ -152,7 +152,7 @@ export function AttachmentPicker({
|
||||
>
|
||||
<FileIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
||||
<span className="text-xs text-gray-800 truncate flex-1">{a.filename}</span>
|
||||
<span className="text-[10px] text-gray-400 shrink-0">{api.formatSize(a.size)}</span>
|
||||
<span className="text-3xs text-gray-400 shrink-0">{api.formatSize(a.size)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(a)}
|
||||
|
||||
@ -118,7 +118,7 @@ export default function BackgroundPicker() {
|
||||
style={{ backgroundImage: 'var(--bg-image)', backgroundSize: 'cover' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="absolute bottom-0 inset-x-0 text-[10px] py-0.5 bg-black/45 text-white">
|
||||
<span className="absolute bottom-0 inset-x-0 text-3xs py-0.5 bg-black/45 text-white">
|
||||
{p.label}
|
||||
</span>
|
||||
</button>
|
||||
@ -152,11 +152,11 @@ export default function BackgroundPicker() {
|
||||
<img src={imageDataUrl} alt="背景预览" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-2xs text-gray-500">
|
||||
图片会等比缩放到最长边 2560px 后保存在本机(上限约 2.4MB)。
|
||||
</p>
|
||||
{error && (
|
||||
<p role="alert" className="text-[11px] text-red-600">
|
||||
<p role="alert" className="text-2xs text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
@ -186,7 +186,7 @@ export default function BackgroundPicker() {
|
||||
onChange={setBlur}
|
||||
/>
|
||||
{/* 配额保护的下限提示:告诉用户上限是怎么来的,而不是神秘失败 */}
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-2xs text-gray-500">
|
||||
当前背景数据约 {Math.round((imageDataUrl.length || 1) / 1024)}KB,上限{' '}
|
||||
{Math.round(MAX_DATA_URL_BYTES / 1024)}KB。
|
||||
</p>
|
||||
@ -232,7 +232,7 @@ function Slider({
|
||||
className="w-full mt-1 accent-blue-600"
|
||||
aria-label={label}
|
||||
/>
|
||||
<span className="block text-[11px] text-gray-500">{hint}</span>
|
||||
<span className="block text-2xs text-gray-500">{hint}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@ -465,7 +465,7 @@ function MonthGrid({
|
||||
{/* 农历日必须显示:农历重复规则的公历日期每次都在变,
|
||||
不显示农历人无法确认「每月十五」到底落在哪一格 */}
|
||||
<span
|
||||
className={`text-[10px] leading-none truncate ${
|
||||
className={`text-3xs leading-none truncate ${
|
||||
outside ? 'text-gray-300' : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
@ -535,7 +535,7 @@ function WeekGrid({
|
||||
>
|
||||
{d.getMonth() + 1}.{d.getDate()}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-400 leading-none">{cellLunarLabel(d)}</div>
|
||||
<div className="text-3xs text-gray-400 leading-none">{cellLunarLabel(d)}</div>
|
||||
</div>
|
||||
<div className="flex-1 p-1 flex flex-col gap-1">
|
||||
{list.length === 0 ? (
|
||||
|
||||
@ -189,7 +189,7 @@ export default function ComposePage() {
|
||||
placeholder="refactor-auth"
|
||||
className="w-full text-sm border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400"
|
||||
/>
|
||||
{aliasError && <span className="text-[10px] text-red-600">{aliasError}</span>}
|
||||
{aliasError && <span className="text-3xs text-red-600">{aliasError}</span>}
|
||||
</Field>
|
||||
)}
|
||||
|
||||
@ -227,11 +227,11 @@ export default function ComposePage() {
|
||||
placeholder={defaultRoundsHint}
|
||||
className="w-24 text-sm border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-400">
|
||||
<span className="text-2xs text-gray-400">
|
||||
个来回后 Agent 停止主动发信(自动转发的总结与权限询问不占预算)
|
||||
</span>
|
||||
</div>
|
||||
{roundsError && <span className="text-[10px] text-red-600">{roundsError}</span>}
|
||||
{roundsError && <span className="text-3xs text-red-600">{roundsError}</span>}
|
||||
</Field>
|
||||
)}
|
||||
|
||||
@ -251,7 +251,7 @@ export default function ComposePage() {
|
||||
|
||||
<div className="shrink-0 lg:flex-1 lg:min-h-0 px-4 md:px-6 py-3 flex flex-col">
|
||||
<div className="shrink-0 flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[11px] font-medium text-gray-500">正文(Markdown)</span>
|
||||
<span className="text-2xs font-medium text-gray-500">正文(Markdown)</span>
|
||||
<div className="flex-1" />
|
||||
<Toggle active={!preview} onClick={() => setPreview(false)}>
|
||||
编辑
|
||||
@ -319,8 +319,8 @@ function Field({
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<label className="text-[11px] font-medium text-gray-500">{label}</label>
|
||||
{hint && <span className="text-[10px] text-gray-400">{hint}</span>}
|
||||
<label className="text-2xs font-medium text-gray-500">{label}</label>
|
||||
{hint && <span className="text-3xs text-gray-400">{hint}</span>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
@ -339,7 +339,7 @@ function Toggle({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`tap text-[11px] px-2 py-0.5 rounded ${
|
||||
className={`tap text-2xs px-2 py-0.5 rounded ${
|
||||
active ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -81,7 +81,7 @@ export default function ContactPanel() {
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleArchivedView}
|
||||
className={`tap text-[11px] px-1.5 py-0.5 rounded ${
|
||||
className={`tap text-2xs px-1.5 py-0.5 rounded ${
|
||||
showArchived ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
@ -137,7 +137,7 @@ export default function ContactPanel() {
|
||||
|
||||
{showArchived && (
|
||||
<div className="pt-3 mt-2 border-t border-gray-200">
|
||||
<p className="px-2 pb-1 text-[11px] font-medium text-gray-400">
|
||||
<p className="px-2 pb-1 text-2xs font-medium text-gray-400">
|
||||
已归档 {archivedContacts.length}
|
||||
</p>
|
||||
{archivedContacts.map(c => (
|
||||
@ -146,7 +146,7 @@ export default function ContactPanel() {
|
||||
className="px-3 py-2 rounded-lg opacity-60 hover:opacity-100 hover:bg-gray-50"
|
||||
>
|
||||
<p className="text-xs font-mono text-gray-500 truncate">{c.address}</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
<p className="text-3xs text-gray-500 mt-0.5">
|
||||
{c.mail_count} 封 · 已归档
|
||||
</p>
|
||||
</div>
|
||||
@ -181,20 +181,20 @@ function ArchiveConfirm({
|
||||
<p className="text-xs text-gray-800">
|
||||
归档 <span className="font-mono">{contact.address}</span>?
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-500 mt-0.5">
|
||||
<p className="text-3xs text-gray-500 mt-0.5">
|
||||
对应 Agent 的 session 将被归档,此列表与邮箱界面同时移除
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="tap inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-red-600 text-white text-[11px] font-medium hover:bg-red-700"
|
||||
className="tap inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-red-600 text-white text-2xs font-medium hover:bg-red-700"
|
||||
>
|
||||
<CheckIcon className="w-3 h-3" />
|
||||
确认归档
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="tap inline-flex items-center gap-1 px-2.5 py-1 rounded-md border border-gray-300 text-gray-600 text-[11px] hover:bg-white"
|
||||
className="tap inline-flex items-center gap-1 px-2.5 py-1 rounded-md border border-gray-300 text-gray-600 text-2xs hover:bg-white"
|
||||
>
|
||||
<CloseIcon className="w-3 h-3" />
|
||||
取消
|
||||
@ -235,20 +235,20 @@ function ContactRow({
|
||||
<span className="text-xs font-semibold text-gray-900 truncate">
|
||||
{contact.agent_name}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 font-mono truncate">{contact.path}</span>
|
||||
<span className="text-3xs text-gray-500 font-mono truncate">{contact.path}</span>
|
||||
{contact.unread_count > 0 && (
|
||||
<span className="ml-auto shrink-0 min-w-[16px] h-4 px-1 rounded-full bg-blue-600 text-white text-[9px] font-bold flex items-center justify-center">
|
||||
<span className="ml-auto shrink-0 min-w-[16px] h-4 px-1 rounded-full bg-blue-600 text-white text-3xs font-bold flex items-center justify-center">
|
||||
{contact.unread_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<ChevronRightIcon className="w-3 h-3 text-blue-400 shrink-0" />
|
||||
<span className="text-[11px] text-blue-600 font-mono truncate">
|
||||
<span className="text-2xs text-blue-600 font-mono truncate">
|
||||
{contact.session_alias || '(未命名会话)'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
<p className="text-3xs text-gray-500 mt-0.5">
|
||||
{contact.mail_count} 封 · {time}
|
||||
</p>
|
||||
</button>
|
||||
@ -257,7 +257,7 @@ function ContactRow({
|
||||
<button
|
||||
onClick={onCompose}
|
||||
title="写信给该地址"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-[10px] text-gray-600 hover:bg-white"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-3xs text-gray-600 hover:bg-white"
|
||||
>
|
||||
<ComposeIcon className="w-3 h-3" />
|
||||
写信
|
||||
@ -265,7 +265,7 @@ function ContactRow({
|
||||
<button
|
||||
onClick={onRequestArchive}
|
||||
title="归档该 name@path.session"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-[10px] text-gray-600 hover:bg-white hover:text-red-600 hover:border-red-300"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-3xs text-gray-600 hover:bg-white hover:text-red-600 hover:border-red-300"
|
||||
>
|
||||
<ArchiveIcon className="w-3 h-3" />
|
||||
归档
|
||||
|
||||
@ -48,7 +48,7 @@ function NewKeyBanner({ token, onDismiss }: { token: string; onDismiss: () => vo
|
||||
密钥已创建。全文仅显示这一次,关闭后无法再次查看。
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-[11px] font-mono bg-white border border-amber-200 rounded px-2 py-1.5 break-all">
|
||||
<code className="flex-1 text-2xs font-mono bg-white border border-amber-200 rounded px-2 py-1.5 break-all">
|
||||
{token}
|
||||
</code>
|
||||
<button
|
||||
@ -121,7 +121,7 @@ function CreateForm({ variant, busy, onSubmit }: CreateFormProps) {
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-gray-900">{KEY_TYPE_LABEL[t]}</div>
|
||||
<div className="text-[10px] text-gray-500 mt-0.5">{KEY_TYPE_HINT[t]}</div>
|
||||
<div className="text-3xs text-gray-500 mt-0.5">{KEY_TYPE_HINT[t]}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -142,7 +142,7 @@ function CreateForm({ variant, busy, onSubmit }: CreateFormProps) {
|
||||
onChange={e => setHours(Math.max(1, Number(e.target.value) || 1))}
|
||||
className="w-20 text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-500">小时后过期</span>
|
||||
<span className="text-2xs text-gray-500">小时后过期</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -220,7 +220,7 @@ export default function KeyPanel({
|
||||
<CreateForm variant={variant} busy={loading} onSubmit={onCreate} />
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-2xs text-gray-500">
|
||||
{variant === 'agent'
|
||||
? 'Agent 用该密钥注册、收发邮件与订阅通知。插件首次安装会在本地生成一把密钥并打印出来,把它填到「登记」框即可。'
|
||||
: '第三方客户端用该密钥访问自己的邮箱(Authorization: Bearer)。它不能用于注册 Agent。'}
|
||||
@ -238,21 +238,21 @@ export default function KeyPanel({
|
||||
const agentKey = variant === 'agent' ? (k as api.AgentKey) : null;
|
||||
return (
|
||||
<div key={k.key_id} className="px-3 py-2.5 flex items-center gap-x-3 gap-y-1 flex-wrap">
|
||||
<code className="text-[11px] font-mono text-gray-700 w-24 shrink-0">
|
||||
<code className="text-2xs font-mono text-gray-700 w-24 shrink-0">
|
||||
{k.token_hint}
|
||||
</code>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs text-gray-900 truncate">
|
||||
{k.label || <span className="text-gray-400">(无备注)</span>}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-500 mt-0.5">
|
||||
<div className="text-3xs text-gray-500 mt-0.5">
|
||||
{KEY_TYPE_LABEL[k.key_type]}
|
||||
{k.expires_at && ` · ${new Date(k.expires_at).toLocaleString()} 过期`}
|
||||
{agentKey &&
|
||||
(agentKey.agent_name ? ` · ${agentKey.agent_name}` : ' · 待绑定')}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-[10px] shrink-0 ${st.cls}`}>{st.text}</span>
|
||||
<span className={`text-3xs shrink-0 ${st.cls}`}>{st.text}</span>
|
||||
|
||||
{agentKey && onBind && bindingID === k.key_id ? (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
@ -260,7 +260,7 @@ export default function KeyPanel({
|
||||
value={bindName}
|
||||
onChange={e => setBindName(e.target.value)}
|
||||
placeholder="Agent 名"
|
||||
className="w-28 text-[11px] border border-gray-300 rounded px-1.5 py-1"
|
||||
className="w-28 text-2xs border border-gray-300 rounded px-1.5 py-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -268,13 +268,13 @@ export default function KeyPanel({
|
||||
setBindingID(null);
|
||||
setBindName('');
|
||||
}}
|
||||
className="text-[11px] text-blue-600 hover:underline"
|
||||
className="text-2xs text-blue-600 hover:underline"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBindingID(null)}
|
||||
className="text-[11px] text-gray-500 hover:underline"
|
||||
className="text-2xs text-gray-500 hover:underline"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@ -287,7 +287,7 @@ export default function KeyPanel({
|
||||
setBindingID(k.key_id);
|
||||
setBindName(agentKey.agent_name ?? '');
|
||||
}}
|
||||
className="text-[11px] text-gray-500 hover:text-gray-900 shrink-0"
|
||||
className="text-2xs text-gray-500 hover:text-gray-900 shrink-0"
|
||||
>
|
||||
绑定
|
||||
</button>
|
||||
|
||||
@ -53,7 +53,7 @@ export default function LoginPage() {
|
||||
// auto margin 在空间不足时自动退化为 0,于是矮屏变成正常的顶对齐可滚布局。
|
||||
return (
|
||||
<div className="h-full overflow-y-auto flex justify-center bg-slate-100">
|
||||
<div className="w-[380px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
|
||||
<div className="w-[380px] max-w-[92vw] shrink-0 my-auto bg-white rounded-2xl shadow-2 p-6 sm:p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
||||
<BrandMarkIcon className="w-6 h-6" />
|
||||
@ -64,7 +64,7 @@ export default function LoginPage() {
|
||||
|
||||
<form onSubmit={submit} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">用户名</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">用户名</label>
|
||||
<input
|
||||
ref={userRef}
|
||||
value={username}
|
||||
@ -79,7 +79,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">密码</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
|
||||
@ -63,8 +63,8 @@ export default function MailList() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full lg:w-[320px] shrink-0 border-r border-gray-200 bg-white flex flex-col min-w-0">
|
||||
<div className="px-4 py-3 border-b border-gray-200 flex items-center gap-1">
|
||||
<div className="w-full lg:w-[320px] shrink-0 lg:shadow-panel bg-white flex flex-col min-w-0">
|
||||
<div className="px-4 py-3.5 border-b border-gray-200 flex items-center gap-1">
|
||||
<h2 className="text-sm font-semibold text-gray-800">{isSent ? '发件箱' : '收件箱'}</h2>
|
||||
{/* 显示「会话数 · 邮件数」而不是只显示邮件数:分组之后前者才是
|
||||
「有几件事」,后者只是流量 */}
|
||||
@ -75,7 +75,7 @@ export default function MailList() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-0.5">
|
||||
<div className="flex-1 overflow-y-auto p-2.5 space-y-1">
|
||||
{groups.map(g =>
|
||||
isFlatGroup(g) ? (
|
||||
<MailItem
|
||||
@ -157,7 +157,7 @@ function SessionGroup({
|
||||
>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full text-left px-3 py-2.5 rounded-lg hover:bg-gray-50"
|
||||
className="w-full text-left px-3.5 py-3 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronRightIcon
|
||||
@ -173,18 +173,18 @@ function SessionGroup({
|
||||
{showTo ? '→ ' : ''}
|
||||
{peer}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 shrink-0">{time}</span>
|
||||
<span className="text-3xs text-gray-500 shrink-0">{time}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 mt-0.5 pl-5">
|
||||
{g.unreadCount > 0 && (
|
||||
<span className="shrink-0 min-w-[16px] h-4 px-1 rounded-full bg-blue-600 text-white text-[9px] font-bold flex items-center justify-center">
|
||||
<span className="shrink-0 min-w-[16px] h-4 px-1 rounded-full bg-blue-600 text-white text-3xs font-bold flex items-center justify-center">
|
||||
{g.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`text-xs truncate ${
|
||||
g.unreadCount > 0 ? 'font-medium text-gray-900' : 'text-gray-500'
|
||||
g.unreadCount > 0 ? 'font-semibold text-gray-900' : 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{g.subject}
|
||||
@ -192,10 +192,10 @@ function SessionGroup({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-0.5 pl-5">
|
||||
<span className="text-[10px] text-blue-500 font-mono truncate">
|
||||
<span className="text-3xs text-blue-600 font-mono truncate">
|
||||
{g.alias ? `.${g.alias}` : '(未命名会话)'}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 shrink-0">{g.mails.length} 封</span>
|
||||
<span className="text-3xs text-gray-500 shrink-0">{g.mails.length} 封</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@ -276,14 +276,14 @@ function MailItem({
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 shrink-0">{time}</span>
|
||||
<span className="text-3xs text-gray-500 shrink-0">{time}</span>
|
||||
</div>
|
||||
|
||||
{!compact && (
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
{isUnread && <span className="w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />}
|
||||
{isPermission && (
|
||||
<span className="shrink-0 inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-orange-100 text-orange-700 text-[9px] font-medium">
|
||||
<span className="shrink-0 inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-orange-100 text-orange-700 text-3xs font-medium">
|
||||
<ShieldIcon className="w-2.5 h-2.5" />
|
||||
权限
|
||||
</span>
|
||||
@ -303,7 +303,7 @@ function MailItem({
|
||||
发件箱里仍可能出现权限邮件(理论上人发不出,但不假设数据一定干净) */}
|
||||
{compact && isPermission && (
|
||||
<span
|
||||
className={`inline-flex items-center gap-0.5 text-[10px] ${
|
||||
className={`inline-flex items-center gap-0.5 text-3xs ${
|
||||
mail.permission_result ? 'text-gray-400' : 'text-orange-600 font-medium'
|
||||
}`}
|
||||
>
|
||||
@ -312,11 +312,11 @@ function MailItem({
|
||||
</span>
|
||||
)}
|
||||
{!compact && mail.session_alias && (
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] text-blue-600 font-mono">.{mail.session_alias}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-3xs text-blue-600 font-mono">.{mail.session_alias}</span>
|
||||
)}
|
||||
{ccCount > 0 && <span className="text-[10px] text-gray-400">抄送 {ccCount}</span>}
|
||||
{ccCount > 0 && <span className="text-3xs text-gray-500">抄送 {ccCount}</span>}
|
||||
{attachCount > 0 && (
|
||||
<span className="inline-flex items-center gap-0.5 text-[10px] text-gray-400">
|
||||
<span className="inline-flex items-center gap-0.5 text-3xs text-gray-500">
|
||||
<PaperclipIcon className="w-2.5 h-2.5" />
|
||||
{attachCount}
|
||||
</span>
|
||||
|
||||
@ -175,7 +175,7 @@ function PermissionEditor() {
|
||||
setEditing(false);
|
||||
}}
|
||||
title={o.desc}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] font-medium border transition-colors disabled:opacity-40 ${
|
||||
className={`px-1.5 py-0.5 rounded text-3xs font-medium border transition-colors disabled:opacity-40 ${
|
||||
mode === o.value
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-gray-200 text-gray-500 hover:bg-gray-50'
|
||||
@ -184,7 +184,7 @@ function PermissionEditor() {
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
<button onClick={() => setEditing(false)} className="text-[10px] text-gray-400 hover:text-gray-700 ml-0.5">
|
||||
<button onClick={() => setEditing(false)} className="text-3xs text-gray-400 hover:text-gray-700 ml-0.5">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@ -226,7 +226,7 @@ function BudgetEditor() {
|
||||
<button
|
||||
onClick={open}
|
||||
title="本任务的往返预算:Agent 主动发信的次数上限(自动转发的总结与权限询问不占用)"
|
||||
className={`inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded border transition-colors ${
|
||||
className={`inline-flex items-center gap-1 text-2xs px-2 py-0.5 rounded border transition-colors ${
|
||||
exhausted
|
||||
? 'border-red-200 bg-red-50 text-red-700'
|
||||
: 'border-gray-200 text-gray-500 hover:border-blue-300 hover:text-blue-600'
|
||||
@ -244,7 +244,7 @@ function BudgetEditor() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[11px] text-gray-500">往返预算</span>
|
||||
<span className="text-2xs text-gray-500">往返预算</span>
|
||||
<input
|
||||
value={draft}
|
||||
onChange={e => setDraft(e.target.value)}
|
||||
@ -258,7 +258,7 @@ function BudgetEditor() {
|
||||
<button
|
||||
disabled={busy || invalid}
|
||||
onClick={() => commit({ max_rounds: draft.trim() === '' ? 0 : Number(draft.trim()) })}
|
||||
className="text-[11px] px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40"
|
||||
className="text-2xs px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
@ -266,13 +266,13 @@ function BudgetEditor() {
|
||||
disabled={busy || budget.used_rounds === 0}
|
||||
onClick={() => commit({ reset: true })}
|
||||
title="已用次数归零,上限不变"
|
||||
className="text-[11px] text-gray-500 hover:text-gray-900 disabled:opacity-30"
|
||||
className="text-2xs text-gray-500 hover:text-gray-900 disabled:opacity-30"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditing(false)}
|
||||
className="text-[11px] text-gray-400 hover:text-gray-700"
|
||||
className="text-2xs text-gray-400 hover:text-gray-700"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@ -308,9 +308,9 @@ function RenameProposalBar() {
|
||||
<span className="font-mono font-semibold">.{proposal.alias}</span>
|
||||
</p>
|
||||
{proposal.reason && (
|
||||
<p className="text-[11px] text-blue-700 mt-0.5">{proposal.reason}</p>
|
||||
<p className="text-2xs text-blue-700 mt-0.5">{proposal.reason}</p>
|
||||
)}
|
||||
<p className="text-[10px] text-blue-500 mt-0.5">
|
||||
<p className="text-3xs text-blue-700 mt-0.5">
|
||||
改名后需用 name@path.{proposal.alias} 寻址;旧别名立即失效。
|
||||
接受后此别名不再被 Agent 平台的自动命名覆盖
|
||||
</p>
|
||||
@ -376,13 +376,13 @@ function ForwardBar({ mail, onClose }: { mail: Mail; onClose: () => void }) {
|
||||
<div className="shrink-0 max-h-[min(65vh,calc(var(--app-height)-3rem))] overflow-y-auto overscroll-contain border-t border-gray-200 bg-white px-4 md:px-6 py-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ForwardIcon className="w-3.5 h-3.5 text-gray-500" />
|
||||
<span className="text-[11px] font-medium text-gray-600">
|
||||
<span className="text-2xs font-medium text-gray-600">
|
||||
转发「{mail.subject}」
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={() => setCcOpen(o => !o)}
|
||||
className={`tap text-[10px] ${ccOpen ? 'text-blue-600' : 'text-gray-500 hover:text-blue-600'}`}
|
||||
className={`tap text-3xs ${ccOpen ? 'text-blue-600' : 'text-gray-500 hover:text-blue-600'}`}
|
||||
>
|
||||
{ccOpen ? '收起抄送' : '抄送'}
|
||||
</button>
|
||||
@ -468,19 +468,19 @@ function Header({
|
||||
<BackButton />
|
||||
<h2 className="text-sm font-semibold text-gray-900 min-w-0 break-words">{mail.subject}</h2>
|
||||
{mail.status === 'unread' && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 text-[10px] font-medium">
|
||||
<span className="px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 text-3xs font-medium">
|
||||
未读
|
||||
</span>
|
||||
)}
|
||||
{mail.mail_type === 'permission_request' && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-orange-100 text-orange-700 text-[10px] font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-orange-100 text-orange-700 text-3xs font-medium">
|
||||
<ShieldIcon className="w-3 h-3" />
|
||||
权限请求
|
||||
</span>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{mail.status === 'unread' && (
|
||||
<button onClick={onRead} className="tap text-xs text-blue-500 hover:underline">
|
||||
<button onClick={onRead} className="tap text-xs text-blue-600 hover:underline">
|
||||
标记已读
|
||||
</button>
|
||||
)}
|
||||
@ -553,20 +553,20 @@ function ThreadCard({ mail, onForward }: { mail: Mail; onForward?: () => void })
|
||||
都渲染成 human 就分不清谁说的话 */}
|
||||
<span className="font-semibold text-gray-800 font-mono">{mail.from_name}</span>
|
||||
{isPermission && (
|
||||
<span className="px-1 py-0.5 rounded bg-orange-200 text-orange-800 text-[9px] font-medium">
|
||||
<span className="px-1 py-0.5 rounded bg-orange-200 text-orange-800 text-3xs font-medium">
|
||||
权限请求
|
||||
</span>
|
||||
)}
|
||||
{mail.cc_list?.length > 0 && (
|
||||
<span
|
||||
className="text-[10px] text-gray-400"
|
||||
className="text-3xs text-gray-400"
|
||||
title={mail.cc_list.map(c => c.raw || `${c.name}@${c.path || ''}`).join(', ')}
|
||||
>
|
||||
抄送 {mail.cc_list.length}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<span className="text-[10px] text-gray-400">{time}</span>
|
||||
<span className="text-3xs text-gray-400">{time}</span>
|
||||
{onForward && (
|
||||
<button
|
||||
onClick={onForward}
|
||||
@ -628,7 +628,7 @@ export function PermissionPanel({ mail }: { mail: Mail }) {
|
||||
*/
|
||||
const staleBanner =
|
||||
staleWarning || expiredBeforeDecide ? (
|
||||
<p className="mt-2 text-[11px] leading-relaxed text-amber-800 bg-amber-50 border border-amber-200 rounded-md px-2.5 py-1.5">
|
||||
<p className="mt-2 text-2xs leading-relaxed text-amber-800 bg-amber-50 border border-amber-200 rounded-md px-2.5 py-1.5">
|
||||
{staleWarning ||
|
||||
'已超过等待窗口,发起它的 Agent 很可能已不再阻塞等待。现在批准不会恢复当时那次工具调用 —— 决策会作为一条通知投给它,让它重起一轮。'}
|
||||
</p>
|
||||
@ -678,7 +678,7 @@ export function PermissionPanel({ mail }: { mail: Mail }) {
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-orange-200">
|
||||
{staleBanner}
|
||||
<div className="text-[11px] text-gray-500 mb-2">
|
||||
<div className="text-2xs text-gray-500 mb-2">
|
||||
{options.length === 0
|
||||
? '这题没有预设选项,请直接填写回答:'
|
||||
: multi ? '可多选,也可补充说明:' : '请选择一项,也可补充说明:'}
|
||||
@ -725,7 +725,7 @@ export function PermissionPanel({ mail }: { mail: Mail }) {
|
||||
提交回答
|
||||
</button>
|
||||
{blank && (
|
||||
<span className="text-[11px] text-gray-400">请先选择或填写回答</span>
|
||||
<span className="text-2xs text-gray-400">请先选择或填写回答</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -852,19 +852,19 @@ function ReplyBar({
|
||||
return (
|
||||
<div className="shrink-0 max-h-[min(65vh,calc(var(--app-height)-3rem))] overflow-y-auto overscroll-contain border-t border-gray-200 bg-white px-4 md:px-6 py-3">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<p className="text-[10px] text-gray-400 font-mono min-w-0 truncate">回复 {target}</p>
|
||||
<p className="text-3xs text-gray-400 font-mono min-w-0 truncate">回复 {target}</p>
|
||||
<div className="flex-1" />
|
||||
{(replyTo.cc_list?.length ?? 0) > 0 && (
|
||||
<button
|
||||
onClick={replyAll}
|
||||
className="tap text-[10px] text-gray-500 hover:text-blue-600"
|
||||
className="tap text-3xs text-gray-500 hover:text-blue-600"
|
||||
>
|
||||
回复全部
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setCcOpen(o => !o)}
|
||||
className={`tap text-[10px] ${ccOpen ? 'text-blue-600' : 'text-gray-500 hover:text-blue-600'}`}
|
||||
className={`tap text-3xs ${ccOpen ? 'text-blue-600' : 'text-gray-500 hover:text-blue-600'}`}
|
||||
>
|
||||
{ccOpen ? '收起抄送' : '抄送'}
|
||||
</button>
|
||||
@ -898,7 +898,7 @@ function ReplyBar({
|
||||
setBudgetEditing(true);
|
||||
}}
|
||||
title="本任务往返预算:Agent 主动发信上限(自动转发与权限询问不占)"
|
||||
className={`tap inline-flex items-center gap-1 text-[10px] px-2 py-1 rounded border ${
|
||||
className={`tap inline-flex items-center gap-1 text-3xs px-2 py-1 rounded border ${
|
||||
budget.unlimited
|
||||
? 'border-gray-200 text-gray-500 hover:border-blue-300'
|
||||
: budget.remaining === 0
|
||||
@ -914,7 +914,7 @@ function ReplyBar({
|
||||
)}
|
||||
{budget && budgetEditing && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-[10px] text-gray-500">预算</span>
|
||||
<span className="text-3xs text-gray-500">预算</span>
|
||||
<input
|
||||
value={maxRoundsDraft}
|
||||
onChange={e => setMaxRoundsDraft(e.target.value)}
|
||||
@ -931,13 +931,13 @@ function ReplyBar({
|
||||
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"
|
||||
className="tap px-2 py-0.5 text-3xs 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"
|
||||
className="tap text-3xs text-gray-400 hover:text-gray-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@ -972,5 +972,5 @@ function StatusBadge({ status }: { status: string }) {
|
||||
archived: { label: '已归档', cls: 'bg-gray-200 text-gray-600' }
|
||||
};
|
||||
const b = map[status] || map.active;
|
||||
return <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${b.cls}`}>{b.label}</span>;
|
||||
return <span className={`text-3xs px-1.5 py-0.5 rounded-full ${b.cls}`}>{b.label}</span>;
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ function PanelHeader({ count }: { count: number }) {
|
||||
<h3 className="text-sm font-semibold text-gray-900">Agent 模型范围</h3>
|
||||
{count > 0 && <span className="text-xs text-gray-400">{count}</span>}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-2xs text-gray-500">
|
||||
划定每个平台在邮件场景下可用的模型。勾选顺序即尝试顺序 —— 插件按序降级,
|
||||
全部失败才回一封说明失败原因的邮件。
|
||||
<br />
|
||||
@ -152,12 +152,12 @@ function AgentModelRow({
|
||||
className={`w-3 h-3 text-gray-400 shrink-0 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
<span className="text-xs font-mono text-gray-900 w-32 shrink-0 truncate">{agentName}</span>
|
||||
<span className="min-w-0 flex-1 text-[11px] text-gray-500">
|
||||
<span className="min-w-0 flex-1 text-2xs text-gray-500">
|
||||
{saved.length === 0 ? '不限定(用平台默认模型)' : `${saved.length} 个模型,按序尝试`}
|
||||
</span>
|
||||
{staleKeys.length > 0 && (
|
||||
<span
|
||||
className="shrink-0 px-1 py-0.5 rounded bg-amber-100 text-amber-700 text-[9px]"
|
||||
className="shrink-0 px-1 py-0.5 rounded bg-amber-100 text-amber-700 text-3xs"
|
||||
title="已选但平台当前没有上报这些模型"
|
||||
>
|
||||
{staleKeys.length} 个已失效
|
||||
@ -176,7 +176,7 @@ function AgentModelRow({
|
||||
{err && <p className="text-xs text-red-600 pt-3">{err}</p>}
|
||||
|
||||
{!loading && catalog.length === 0 && staleKeys.length === 0 && (
|
||||
<p className="text-[11px] text-gray-500 pt-3">
|
||||
<p className="text-2xs text-gray-500 pt-3">
|
||||
该平台还没有上报模型清单。插件会在心跳时上报(约 30 秒一次)——
|
||||
若长时间为空,检查插件是否在运行、以及它能否读到平台的 provider 配置。
|
||||
</p>
|
||||
@ -184,7 +184,7 @@ function AgentModelRow({
|
||||
|
||||
{picks.length > 0 && (
|
||||
<div className="pt-3">
|
||||
<p className="text-[10px] font-medium text-gray-500 mb-1.5">
|
||||
<p className="text-3xs font-medium text-gray-500 mb-1.5">
|
||||
尝试顺序(自上而下)
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
@ -198,11 +198,11 @@ function AgentModelRow({
|
||||
isStale ? 'border-amber-200' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<span className="w-4 text-[10px] text-gray-400 shrink-0">{i + 1}</span>
|
||||
<span className="w-4 text-3xs text-gray-400 shrink-0">{i + 1}</span>
|
||||
<span className="text-xs font-mono text-gray-800 truncate">{key}</span>
|
||||
{isStale && (
|
||||
<span
|
||||
className="shrink-0 text-[9px] text-amber-700"
|
||||
className="shrink-0 text-3xs text-amber-700"
|
||||
title="平台当前没有上报这个模型,可能已下线"
|
||||
>
|
||||
已失效
|
||||
@ -241,7 +241,7 @@ function AgentModelRow({
|
||||
|
||||
{catalog.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-gray-500 mb-1.5">
|
||||
<p className="text-3xs font-medium text-gray-500 mb-1.5">
|
||||
平台上报的模型({catalog.length})
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@ -253,7 +253,7 @@ function AgentModelRow({
|
||||
key={key}
|
||||
onClick={() => toggle(key)}
|
||||
title={m.display_name || key}
|
||||
className={`tap px-2 py-1 text-[11px] font-mono rounded border transition-colors ${
|
||||
className={`tap px-2 py-1 text-2xs font-mono rounded border transition-colors ${
|
||||
on
|
||||
? 'bg-blue-50 border-blue-300 text-blue-700'
|
||||
: 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'
|
||||
@ -270,7 +270,7 @@ function AgentModelRow({
|
||||
{(catalog.length > 0 || picks.length > 0) && (
|
||||
<div className="flex items-center gap-2">
|
||||
{picks.length === 0 && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
<span className="text-3xs text-gray-400">
|
||||
未选 = 不限定,用平台默认模型
|
||||
</span>
|
||||
)}
|
||||
@ -278,7 +278,7 @@ function AgentModelRow({
|
||||
{dirty && (
|
||||
<button
|
||||
onClick={() => setPicks(saved)}
|
||||
className="tap text-[11px] text-gray-500 hover:text-gray-900"
|
||||
className="tap text-2xs text-gray-500 hover:text-gray-900"
|
||||
>
|
||||
撤销
|
||||
</button>
|
||||
@ -286,7 +286,7 @@ function AgentModelRow({
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={!dirty || saving}
|
||||
className="tap inline-flex items-center gap-1 px-3 py-1 text-[11px] rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40"
|
||||
className="tap inline-flex items-center gap-1 px-3 py-1 text-2xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40"
|
||||
>
|
||||
{saving ? (
|
||||
<SpinnerIcon className="w-3 h-3 animate-spin" />
|
||||
|
||||
@ -85,10 +85,10 @@ export default function NarrowNav() {
|
||||
}`}
|
||||
>
|
||||
<Icon />
|
||||
<span className="text-[10px] leading-none">{short}</span>
|
||||
<span className="text-3xs leading-none">{short}</span>
|
||||
{badge > 0 && (
|
||||
<span
|
||||
className={`absolute top-1 right-[22%] min-w-[15px] h-[15px] px-1 rounded-full text-[9px] font-bold flex items-center justify-center ${
|
||||
className={`absolute top-1 right-[22%] min-w-[15px] h-[15px] px-1 rounded-full text-3xs font-bold flex items-center justify-center ${
|
||||
mode === 'inbox'
|
||||
? 'bg-red-600 text-white'
|
||||
: mode === 'permissions'
|
||||
@ -111,7 +111,7 @@ export default function NarrowNav() {
|
||||
}`}
|
||||
>
|
||||
<ComposeIcon />
|
||||
<span className="text-[10px] leading-none">新建</span>
|
||||
<span className="text-3xs leading-none">新建</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -128,7 +128,7 @@ export default function NarrowNav() {
|
||||
<ConnectionIndicator />
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] leading-none">我的</span>
|
||||
<span className="text-3xs leading-none">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@ -74,7 +74,7 @@ export default function NarrowStack({
|
||||
|
||||
{mounted && (
|
||||
<div
|
||||
className={`absolute inset-0 z-10 flex bg-white border-l border-gray-200 shadow-2xl transition-transform duration-200 ease-out motion-reduce:transition-none ${
|
||||
className={`absolute inset-0 z-10 flex bg-white shadow-3 transition-transform duration-200 ease-out motion-reduce:transition-none ${
|
||||
entered ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -87,7 +87,7 @@ export default function PermissionChip({ mode, enforcement, compact }: Permissio
|
||||
<span
|
||||
title={`${hint}(强制力:${ENFORCEMENT_LABEL[e]})`}
|
||||
className={`inline-flex items-center gap-1 rounded border font-medium ${color} ${
|
||||
compact ? 'px-1 text-[10px]' : 'px-1.5 text-xs'
|
||||
compact ? 'px-1 text-3xs' : 'px-1.5 text-xs'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
|
||||
@ -58,7 +58,7 @@ export default function PermissionList() {
|
||||
<div className="px-4 py-3 border-b border-gray-200 flex items-center gap-1">
|
||||
<h2 className="text-sm font-semibold text-gray-800">授权</h2>
|
||||
{pendingTotal > 0 ? (
|
||||
<span className="ml-2 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-orange-100 text-orange-700 text-[10px] font-semibold">
|
||||
<span className="ml-2 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-orange-100 text-orange-700 text-3xs font-semibold">
|
||||
<ShieldIcon className="w-2.5 h-2.5" />
|
||||
{pendingTotal} 待决策
|
||||
</span>
|
||||
@ -85,7 +85,7 @@ export default function PermissionList() {
|
||||
<div className="text-center py-10">
|
||||
<ShieldIcon className="w-8 h-8 mx-auto text-gray-300" />
|
||||
<p className="text-xs text-gray-400 mt-2">没有授权请求</p>
|
||||
<p className="text-[10px] text-gray-400 mt-1 px-6">
|
||||
<p className="text-3xs text-gray-500 mt-1 px-6">
|
||||
Agent 执行敏感操作(bash、写文件)时会在这里请求你批准
|
||||
</p>
|
||||
</div>
|
||||
@ -135,28 +135,28 @@ function PermissionSessionGroup({
|
||||
<span className="text-xs font-mono text-gray-900 truncate">
|
||||
{g.agentName}{g.path ? `@${g.path}` : ''}{g.alias ? `.${g.alias}` : ''}
|
||||
</span>
|
||||
<span className="ml-auto text-[10px] text-gray-400 shrink-0">{time}</span>
|
||||
<span className="ml-auto text-3xs text-gray-500 shrink-0">{time}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 mt-1 pl-5">
|
||||
{hasPending ? (
|
||||
<span className="shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-orange-700 text-white text-[9px] font-bold">
|
||||
<span className="shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-orange-700 text-white text-3xs font-bold">
|
||||
<ShieldIcon className="w-2.5 h-2.5" />
|
||||
{g.pending.length} 待决策
|
||||
</span>
|
||||
) : (
|
||||
<span className="shrink-0 inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-gray-100 text-gray-500 text-[9px]">
|
||||
<span className="shrink-0 inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-gray-100 text-gray-500 text-3xs">
|
||||
<CheckIcon className="w-2.5 h-2.5" />
|
||||
已全部处理
|
||||
</span>
|
||||
)}
|
||||
{g.settled.length > 0 && (
|
||||
<span className="text-[10px] text-gray-400">历史 {g.settled.length}</span>
|
||||
<span className="text-3xs text-gray-500">历史 {g.settled.length}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!g.alias && (
|
||||
<p className="text-[10px] text-gray-400 font-mono truncate mt-0.5 pl-5">
|
||||
<p className="text-3xs text-gray-500 font-mono truncate mt-0.5 pl-5">
|
||||
(未命名会话)
|
||||
</p>
|
||||
)}
|
||||
@ -177,7 +177,7 @@ function PermissionSessionGroup({
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowSettled(v => !v)}
|
||||
className="w-full text-left px-2 py-1 text-[10px] text-gray-400 hover:text-gray-600"
|
||||
className="w-full text-left px-2 py-1 text-3xs text-gray-500 hover:text-gray-600"
|
||||
>
|
||||
{showSettled ? '收起' : '展开'}已决策 {g.settled.length}
|
||||
</button>
|
||||
@ -248,12 +248,12 @@ function PermissionRow({
|
||||
>
|
||||
{mail.subject}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 shrink-0">{time}</span>
|
||||
<span className="text-3xs text-gray-500 shrink-0">{time}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
{settled ? (
|
||||
<span
|
||||
className={`inline-flex items-center gap-0.5 text-[10px] ${
|
||||
className={`inline-flex items-center gap-0.5 text-3xs ${
|
||||
approved ? 'text-green-600' : 'text-red-500'
|
||||
}`}
|
||||
>
|
||||
@ -262,7 +262,7 @@ function PermissionRow({
|
||||
</span>
|
||||
) : expired ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 text-[10px] text-gray-500"
|
||||
className="inline-flex items-center gap-0.5 text-3xs text-gray-500"
|
||||
title={
|
||||
'已超过等待窗口,发起它的 Agent 很可能已不再阻塞等待。' +
|
||||
'现在批准不会恢复当时那次工具调用 —— 决策会作为一条通知投给它,让它重起一轮。'
|
||||
@ -272,7 +272,7 @@ function PermissionRow({
|
||||
可能已失效
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-0.5 text-[10px] text-orange-600 font-medium">
|
||||
<span className="inline-flex items-center gap-0.5 text-3xs text-orange-600 font-medium">
|
||||
<ShieldIcon className="w-2.5 h-2.5" />
|
||||
等待你决策
|
||||
</span>
|
||||
|
||||
@ -103,7 +103,7 @@ export default function QuotaPanel() {
|
||||
? 'bg-red-50 text-red-600'
|
||||
: 'bg-gray-50 text-gray-500';
|
||||
const label = st === 'online' ? '在线' : st === 'disabled' ? '已停用' : '离线';
|
||||
return <span className={`text-[10px] px-1.5 py-0.5 rounded ${cls}`}>{label}</span>;
|
||||
return <span className={`text-3xs px-1.5 py-0.5 rounded ${cls}`}>{label}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -114,7 +114,7 @@ export default function QuotaPanel() {
|
||||
<span className="text-xs text-gray-400">{stats.length}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-2xs text-gray-500">
|
||||
<strong className="font-medium text-gray-600">默认预算</strong>:派给某个 Agent 的新任务默认多少个来回(填 0 = 不限)。
|
||||
这只是默认值 —— 写信时可以单独指定,之后在对话页里还能随时调整。
|
||||
插件自动转发的最终总结与权限询问不占用预算。
|
||||
@ -127,8 +127,8 @@ export default function QuotaPanel() {
|
||||
<span className="text-gray-400">邮件与会话保留(历史是审计凭据),此名字今后不可再注册。</span>
|
||||
</p>
|
||||
|
||||
{error && <div className="text-[11px] text-red-600 bg-red-50 rounded px-2 py-1.5">{error}</div>}
|
||||
{notice && <div className="text-[11px] text-green-700 bg-green-50 rounded px-2 py-1.5">{notice}</div>}
|
||||
{error && <div className="text-2xs text-red-600 bg-red-50 rounded px-2 py-1.5">{error}</div>}
|
||||
{notice && <div className="text-2xs text-green-700 bg-green-50 rounded px-2 py-1.5">{notice}</div>}
|
||||
|
||||
{stats.length === 0 ? (
|
||||
<div className="text-xs text-gray-400 py-3">暂无已注册的 Agent</div>
|
||||
@ -149,7 +149,7 @@ export default function QuotaPanel() {
|
||||
|
||||
{statusBadge(s)}
|
||||
|
||||
<div className="min-w-0 flex-1 text-[11px] text-gray-500">
|
||||
<div className="min-w-0 flex-1 text-2xs text-gray-500">
|
||||
{s.default_rounds === 0 ? '默认不限来回' : `默认 ${s.default_rounds} 个来回`}
|
||||
<span className="text-gray-400">
|
||||
{' · '}进行中 {s.active_sessions} 个任务 · 累计发信 {s.sent_total}
|
||||
@ -177,19 +177,19 @@ export default function QuotaPanel() {
|
||||
{/* 停用/恢复/删除 按钮 */}
|
||||
{isConfirming ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-gray-500">确认{confirmAction === 'delete' ? '删除' : ''}?</span>
|
||||
<span className="text-3xs text-gray-500">确认{confirmAction === 'delete' ? '删除' : ''}?</span>
|
||||
<button
|
||||
onClick={() => confirmAction === 'delete'
|
||||
? deleteAgent(s.agent_name)
|
||||
: toggleStatus(s.agent_name, isDisabled)}
|
||||
disabled={busy === s.agent_name}
|
||||
className="tap text-[10px] px-1.5 py-0.5 rounded bg-red-50 text-red-600 hover:bg-red-100"
|
||||
className="tap text-3xs px-1.5 py-0.5 rounded bg-red-50 text-red-600 hover:bg-red-100"
|
||||
>
|
||||
{confirmAction === 'delete' ? '删除' : (isDisabled ? '恢复' : '停用')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setConfirming(null); setConfirmAction(null); }}
|
||||
className="tap text-[10px] text-gray-400 hover:text-gray-600"
|
||||
className="tap text-3xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@ -202,7 +202,7 @@ export default function QuotaPanel() {
|
||||
title={isDisabled
|
||||
? '恢复此 Agent(恢复为离线;停用时撤销的密钥不会自动回来,必须重新签发)'
|
||||
: '停用此 Agent(撤销全部密钥并从补全里隐藏;邮件与会话保留,可恢复)'}
|
||||
className={`tap shrink-0 inline-flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded border ${
|
||||
className={`tap shrink-0 inline-flex items-center gap-1 text-2xs px-1.5 py-0.5 rounded border ${
|
||||
isDisabled
|
||||
? 'border-green-200 text-green-700 hover:bg-green-50'
|
||||
: 'border-gray-200 text-gray-500 hover:text-red-600 hover:border-red-200'
|
||||
@ -215,7 +215,7 @@ export default function QuotaPanel() {
|
||||
onClick={() => { setConfirming(s.agent_name); setConfirmAction('delete'); }}
|
||||
disabled={busy === s.agent_name}
|
||||
title="彻底删除此 Agent(清除密钥与运行态;邮件保留但此名今后不可再用)"
|
||||
className="tap shrink-0 text-[10px] px-1.5 py-0.5 rounded border border-red-200 text-red-400 hover:text-red-600 hover:border-red-300 disabled:opacity-30"
|
||||
className="tap shrink-0 text-3xs px-1.5 py-0.5 rounded border border-red-200 text-red-400 hover:text-red-600 hover:border-red-300 disabled:opacity-30"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
|
||||
@ -53,7 +53,7 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||
// auto margin 在空间不足时自动退化为 0,于是矮屏变成正常的顶对齐可滚布局。
|
||||
return (
|
||||
<div className="h-full overflow-y-auto flex justify-center bg-slate-100">
|
||||
<div className="w-[420px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
|
||||
<div className="w-[420px] max-w-[92vw] shrink-0 my-auto bg-white rounded-2xl shadow-2 p-6 sm:p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
||||
<BrandMarkIcon className="w-6 h-6" />
|
||||
@ -66,7 +66,7 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||
|
||||
<form onSubmit={submit} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">
|
||||
管理员用户名 <span className="text-red-400">(即三维地址的 name 位)</span>
|
||||
</label>
|
||||
<input
|
||||
@ -77,13 +77,13 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||
spellCheck={false}
|
||||
className="w-full text-sm font-mono border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-gray-400">
|
||||
<p className="mt-1 text-3xs text-gray-400">
|
||||
小写字母数字 . _ -,2-64 位;后续可以 `admin@.new` 形式作为收件人
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">显示名</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">显示名</label>
|
||||
<input
|
||||
value={displayName}
|
||||
onChange={e => setDisplayName(e.target.value)}
|
||||
@ -93,7 +93,7 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">
|
||||
密码 <span className="text-red-400">(至少 8 位)</span>
|
||||
</label>
|
||||
<input
|
||||
@ -106,7 +106,7 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-medium text-gray-500 mb-1">确认密码</label>
|
||||
<label className="block text-2xs font-medium text-gray-500 mb-1">确认密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirm}
|
||||
@ -116,7 +116,7 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||
mismatch ? 'border-red-300' : 'border-gray-300 focus:border-blue-400'
|
||||
}`}
|
||||
/>
|
||||
{mismatch && <p className="mt-1 text-[10px] text-red-500">两次输入的密码不一致</p>}
|
||||
{mismatch && <p className="mt-1 text-3xs text-red-500">两次输入的密码不一致</p>}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
@ -80,10 +80,10 @@ export default function Sidebar() {
|
||||
}`}
|
||||
>
|
||||
<Icon />
|
||||
<span className="text-[9px] leading-none">{short}</span>
|
||||
<span className="text-3xs leading-none">{short}</span>
|
||||
{badge > 0 && (
|
||||
<span
|
||||
className={`absolute top-0.5 right-1 min-w-[15px] h-[15px] px-1 rounded-full text-[9px] font-bold flex items-center justify-center ${
|
||||
className={`absolute top-0.5 right-1 min-w-[15px] h-[15px] px-1 rounded-full text-3xs font-bold flex items-center justify-center ${
|
||||
mode === 'inbox'
|
||||
? 'bg-red-600 text-white'
|
||||
: mode === 'permissions'
|
||||
@ -110,14 +110,14 @@ export default function Sidebar() {
|
||||
}`}
|
||||
>
|
||||
<ComposeIcon />
|
||||
<span className="text-[9px] leading-none">新建</span>
|
||||
<span className="text-3xs leading-none">新建</span>
|
||||
</button>
|
||||
|
||||
<div className="mt-2 pt-2 w-full flex flex-col items-center gap-1 border-t border-chrome-700">
|
||||
<button
|
||||
onClick={() => setViewMode('account')}
|
||||
title={`${user?.display_name || user?.username}(点击管理账号)`}
|
||||
className={`relative w-9 h-9 rounded-full flex items-center justify-center text-[11px] font-semibold transition-colors ${
|
||||
className={`relative w-9 h-9 rounded-full flex items-center justify-center text-2xs font-semibold transition-colors ${
|
||||
viewMode === 'account' && !composing
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-chrome-700 text-chrome-200 hover:bg-chrome-600'
|
||||
|
||||
@ -84,7 +84,7 @@ export default function ThemePicker() {
|
||||
title={o.hint}
|
||||
className={`px-3 py-1.5 rounded-md text-xs inline-flex items-center gap-1.5 transition-colors ${
|
||||
active
|
||||
? 'bg-white text-gray-900 shadow-sm border border-gray-200'
|
||||
? 'bg-white text-gray-900 shadow-1'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -249,49 +249,49 @@ function Node({
|
||||
node.from_human ? '' : node.session_workspace || ''
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400">→</span>
|
||||
<span className="text-3xs text-gray-500">→</span>
|
||||
<span className="text-xs font-mono text-gray-500 truncate">{node.to_name}</span>
|
||||
<div className="flex-1" />
|
||||
{isForward && (
|
||||
<span className="px-1 py-0.5 rounded bg-blue-100 text-blue-700 text-[9px]">
|
||||
<span className="px-1 py-0.5 rounded bg-blue-100 text-blue-700 text-3xs">
|
||||
转发
|
||||
</span>
|
||||
)}
|
||||
{node.parent_hidden && (
|
||||
<span
|
||||
className="px-1 py-0.5 rounded bg-gray-100 text-gray-500 text-[9px]"
|
||||
className="px-1 py-0.5 rounded bg-gray-100 text-gray-500 text-3xs"
|
||||
title="上一封不在你的可见范围内"
|
||||
>
|
||||
上游不可见
|
||||
</span>
|
||||
)}
|
||||
{isPermission && (
|
||||
<span className="inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-orange-100 text-orange-700 text-[9px]">
|
||||
<span className="inline-flex items-center gap-0.5 px-1 py-0.5 rounded bg-orange-100 text-orange-700 text-3xs">
|
||||
<ShieldIcon className="w-2.5 h-2.5" />
|
||||
权限
|
||||
</span>
|
||||
)}
|
||||
{node.attachment_count > 0 && (
|
||||
<span className="inline-flex items-center gap-0.5 text-[10px] text-gray-400">
|
||||
<span className="inline-flex items-center gap-0.5 text-3xs text-gray-500">
|
||||
<PaperclipIcon className="w-2.5 h-2.5" />
|
||||
{node.attachment_count}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-gray-400 shrink-0">{time}</span>
|
||||
<span className="text-3xs text-gray-500 shrink-0">{time}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-800 mt-0.5 truncate">{node.subject}</p>
|
||||
{/* 抄送人要显示出来:一封邮件收到两个回复,正是因为它抄送给了两个人。
|
||||
不显示抄送,树上那两个兄弟节点为什么并列就没有解释。 */}
|
||||
{ccCount > 0 && (
|
||||
<p className="text-[10px] text-gray-400 mt-0.5 truncate">
|
||||
<p className="text-3xs text-gray-500 mt-0.5 truncate">
|
||||
抄送 {node.cc_list.map(c => c.raw || `${c.name}@${c.path || ''}${c.session ? '.' + c.session : ''}`).join('、')}
|
||||
</p>
|
||||
)}
|
||||
{node.body_preview && (
|
||||
<p className="text-[11px] text-gray-400 mt-0.5 line-clamp-2">{node.body_preview}</p>
|
||||
<p className="text-2xs text-gray-500 mt-0.5 line-clamp-2">{node.body_preview}</p>
|
||||
)}
|
||||
{node.session_alias && (
|
||||
<span className="text-[10px] text-blue-500 font-mono">.{node.session_alias}</span>
|
||||
<span className="text-3xs text-blue-600 font-mono">.{node.session_alias}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -53,9 +53,9 @@ export function WorkCard({
|
||||
<button onClick={onOpen} className="flex-1 text-left p-3 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-semibold text-gray-900 truncate">{c.agent_name}</span>
|
||||
<span className="text-[10px] text-gray-400 font-mono truncate">{c.path}</span>
|
||||
<span className="text-3xs text-gray-500 font-mono truncate">{c.path}</span>
|
||||
{c.unread_count > 0 && (
|
||||
<span className="ml-auto shrink-0 min-w-[16px] h-4 px-1 rounded-full bg-blue-600 text-white text-[9px] font-bold flex items-center justify-center">
|
||||
<span className="ml-auto shrink-0 min-w-[16px] h-4 px-1 rounded-full bg-blue-600 text-white text-3xs font-bold flex items-center justify-center">
|
||||
{c.unread_count}
|
||||
</span>
|
||||
)}
|
||||
@ -63,7 +63,7 @@ export function WorkCard({
|
||||
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<ChevronRightIcon className="w-3 h-3 text-blue-400 shrink-0" />
|
||||
<span className="text-[11px] text-blue-600 font-mono truncate">
|
||||
<span className="text-2xs text-blue-600 font-mono truncate">
|
||||
{c.session_alias || '(未命名会话)'}
|
||||
</span>
|
||||
</div>
|
||||
@ -80,14 +80,14 @@ export function WorkCard({
|
||||
) : (
|
||||
<BotIcon className="w-3 h-3 text-gray-400 shrink-0 mt-0.5" />
|
||||
)}
|
||||
<p className="text-[11px] text-gray-500 line-clamp-2 leading-snug">
|
||||
<p className="text-2xs text-gray-500 line-clamp-2 leading-snug">
|
||||
{c.last_preview}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-[10px] text-gray-400">
|
||||
<span className="text-3xs text-gray-500">
|
||||
{c.mail_count} 封 · {time}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
@ -100,7 +100,7 @@ export function WorkCard({
|
||||
<button
|
||||
onClick={onCompose}
|
||||
title="写信给该地址"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-[10px] text-gray-600 hover:bg-gray-50"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-3xs text-gray-600 hover:bg-gray-50"
|
||||
>
|
||||
<ComposeIcon className="w-3 h-3" />
|
||||
写信
|
||||
@ -108,7 +108,7 @@ export function WorkCard({
|
||||
<button
|
||||
onClick={onArchive}
|
||||
title="归档该 name@path.session"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-[10px] text-gray-600 hover:bg-gray-50 hover:text-red-600 hover:border-red-300"
|
||||
className="tap inline-flex items-center gap-1 px-2 py-0.5 rounded border border-gray-300 text-3xs text-gray-600 hover:bg-gray-50 hover:text-red-600 hover:border-red-300"
|
||||
>
|
||||
<ArchiveIcon className="w-3 h-3" />
|
||||
归档
|
||||
@ -140,7 +140,7 @@ export function BudgetChip({ max, used }: { max: number; used: number }) {
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-[9px] font-medium shrink-0 ${tone}`}
|
||||
className={`inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-3xs font-medium shrink-0 ${tone}`}
|
||||
title={`往返预算:已用 ${used}/${max}${remaining === 0 ? '(已用尽)' : ''}`}
|
||||
>
|
||||
<GaugeIcon className="w-2.5 h-2.5" />
|
||||
|
||||
@ -38,11 +38,27 @@
|
||||
|
||||
--c-gray-50: 249 250 251;
|
||||
--c-gray-100: 243 244 246;
|
||||
--c-gray-200: 229 231 235;
|
||||
/*
|
||||
* 分隔线刻意比「正牌 gray-200」再淡一档。
|
||||
*
|
||||
* 全站 67 处 `border-*-gray-200` 是「盒子感」的主要来源:硬线把界面切成
|
||||
* 一块块,看着像 2015 年的后台。现代做法是让**留白与表面色差**承担分组,
|
||||
* 线条只留一丝提示。改在令牌上而不是逐个类名,67 处(含 8 处作为底色的
|
||||
* 进度条/骨架)一次性生效,且不会漏。
|
||||
*/
|
||||
--c-gray-200: 234 236 241;
|
||||
--c-gray-300: 209 213 219;
|
||||
/* 最小字号与 placeholder 也会使用这一档;在白底上保持至少 4.5:1。 */
|
||||
--c-gray-400: 107 114 128;
|
||||
--c-gray-500: 107 114 128;
|
||||
/*
|
||||
* gray-500 必须比 gray-400 **深一档**。
|
||||
*
|
||||
* 原来两者都是 107 114 128(完全相同)—— 调色板里因而不存在「比次要文字
|
||||
* 再深一点的中间色」,于是时间戳这类要压在高亮行淡蓝底上的文字没得选,
|
||||
* 只能停在 gray-400:实测在 bg-blue-50 上只有 4.44:1,低于 AA 4.5
|
||||
* (真实渲染量得)。拉开一档后为 5.65:1。
|
||||
*/
|
||||
--c-gray-500: 90 98 112;
|
||||
--c-gray-600: 75 85 99;
|
||||
--c-gray-700: 55 65 81;
|
||||
--c-gray-800: 31 41 55;
|
||||
@ -241,6 +257,11 @@
|
||||
--shadow-1: 0 1px 2px rgb(15 23 42 / 0.06), 0 1px 3px rgb(15 23 42 / 0.1);
|
||||
--shadow-2: 0 2px 4px rgb(15 23 42 / 0.05), 0 4px 12px rgb(15 23 42 / 0.1);
|
||||
--shadow-3: 0 8px 24px rgb(15 23 42 / 0.12), 0 2px 6px rgb(15 23 42 / 0.08);
|
||||
/*
|
||||
* 侧边面板专用:横向偏移 + 大扩散,模拟「两块面板叠在一起」而不是「被线分开」。
|
||||
* 竖向几乎不偏移,否则整列会看起来浮在半空(面板是全高的)。
|
||||
*/
|
||||
--shadow-panel: 1px 0 2px rgb(15 23 42 / 0.04), 4px 0 16px -4px rgb(15 23 42 / 0.08);
|
||||
--hairline: 0 0 0;
|
||||
color-scheme: light;
|
||||
}
|
||||
@ -283,7 +304,7 @@
|
||||
|
||||
--c-gray-50: 17 19 24;
|
||||
--c-gray-100: 32 36 44;
|
||||
--c-gray-200: 44 49 59;
|
||||
--c-gray-200: 39 43 52;
|
||||
--c-gray-300: 61 68 81;
|
||||
--c-gray-400: 138 146 161;
|
||||
--c-gray-500: 165 173 186;
|
||||
@ -321,6 +342,11 @@
|
||||
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.4);
|
||||
--shadow-2: 0 2px 6px rgb(0 0 0 / 0.45);
|
||||
--shadow-3: 0 10px 30px rgb(0 0 0 / 0.55);
|
||||
/*
|
||||
* 深色下层次比浅色更难感知,所以面板阴影要更实一些;
|
||||
* 但仍以「方向性」为主(横偏移),避免整列看起来悬空。
|
||||
*/
|
||||
--shadow-panel: 1px 0 2px rgb(0 0 0 / 0.35), 4px 0 16px -4px rgb(0 0 0 / 0.5);
|
||||
--hairline: 0 0 0 1px rgb(255 255 255 / 0.06);
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user