chore: directory migration - gateway→server, web→client/electron
This commit is contained in:
150
client/electron/src/components/WorkCard.tsx
Normal file
150
client/electron/src/components/WorkCard.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import type { Contact } from '../types';
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ComposeIcon,
|
||||
ChevronRightIcon,
|
||||
GaugeIcon,
|
||||
PersonIcon,
|
||||
BotIcon
|
||||
} from './icons';
|
||||
import PermissionChip from './PermissionChip';
|
||||
|
||||
/**
|
||||
* 工作卡片:中间栏的另一种呈现。
|
||||
*
|
||||
* 与列表行(ContactPanel 的 ContactRow)的分工:
|
||||
* 列表答「跟谁在聊」,卡片答「在聊什么、进展如何」。
|
||||
* 一条线索是一件正在进行的工作,卡片上要能直接看出:
|
||||
* - 主题(多由 Agent 平台的模型生成的摘要)
|
||||
* - 最新一封说了什么、谁说的
|
||||
* - 往返预算还剩多少 —— 预算是任务的属性,快跑满的任务需要人介入
|
||||
*
|
||||
* 容器(列表/滚动/空态)由 ContactPanel 负责:两种视图共用同一份数据与同一套
|
||||
* 打开/写信/归档动作,只有单项的渲染不同。竖向堆叠而非网格 ——
|
||||
* 卡片在中间栏里,320~400px 放不下多列。
|
||||
*/
|
||||
export function WorkCard({
|
||||
contact: c,
|
||||
active,
|
||||
onOpen,
|
||||
onCompose,
|
||||
onArchive
|
||||
}: {
|
||||
contact: Contact;
|
||||
active: boolean;
|
||||
onOpen: () => void;
|
||||
onCompose: () => void;
|
||||
onArchive: () => void;
|
||||
}) {
|
||||
const time = new Date(c.last_activity).toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
const fromHuman = c.last_from !== c.agent_name;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group flex flex-col rounded-lg border bg-white transition-colors ${
|
||||
active ? 'border-blue-300 ring-1 ring-blue-100' : 'border-gray-200 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
{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">
|
||||
{c.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">
|
||||
{c.session_alias || '(未命名会话)'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 主题是这张卡片的主角:它回答「这条线索在干什么」 */}
|
||||
<p className="text-xs text-gray-800 mt-1.5 line-clamp-2 leading-snug">
|
||||
{c.subject || '(无主题)'}
|
||||
</p>
|
||||
|
||||
{c.last_preview && (
|
||||
<div className="flex items-start gap-1 mt-1.5">
|
||||
{fromHuman ? (
|
||||
<PersonIcon className="w-3 h-3 text-gray-400 shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<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">
|
||||
{c.last_preview}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{c.mail_count} 封 · {time}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<PermissionChip mode={c.permission_mode} enforcement={c.permission_enforcement} compact />
|
||||
<BudgetChip max={c.max_rounds} used={c.used_rounds} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="reveal flex gap-1 px-3 pb-2.5">
|
||||
<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"
|
||||
>
|
||||
<ComposeIcon className="w-3 h-3" />
|
||||
写信
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
<ArchiveIcon className="w-3 h-3" />
|
||||
归档
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预算指示条。
|
||||
*
|
||||
* 0 = 不限,此时不显示 —— 一个「0/0」或「不限」的徽标对每张卡片都成立,
|
||||
* 等于纯噪声。只在真正设了上限时才占位置。
|
||||
* 剩 1 个来回时转红:那是需要人介入的时刻(要么加预算,要么让它收尾)。
|
||||
*
|
||||
* 导出供测试单独渲染 —— 它是纯展示件,而通过 WorkCard 渲染要先造一整个 Contact。
|
||||
*/
|
||||
export function BudgetChip({ max, used }: { max: number; used: number }) {
|
||||
if (!max || max <= 0) return null;
|
||||
|
||||
const remaining = Math.max(max - used, 0);
|
||||
const tone =
|
||||
remaining === 0
|
||||
? 'bg-red-100 text-red-700'
|
||||
: remaining <= 1
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-gray-100 text-gray-500';
|
||||
|
||||
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}`}
|
||||
title={`往返预算:已用 ${used}/${max}${remaining === 0 ? '(已用尽)' : ''}`}
|
||||
>
|
||||
<GaugeIcon className="w-2.5 h-2.5" />
|
||||
{remaining}/{max}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user