import { useEffect } from 'react'; import { useContactStore } from '../stores/contactStore'; import { useSessionStore } from '../stores/sessionStore'; import { useUIStore } from '../stores/uiStore'; import { useMailStore } from '../stores/mailStore'; import type { Contact } from '../types'; import { ArchiveIcon, ComposeIcon, CheckIcon, CloseIcon, ChevronRightIcon, ListViewIcon, CardViewIcon } from './icons'; import { WorkCard } from './WorkCard'; /** * 左侧联系人面板:列出所有 name@path.session,支持 * - 点击进入该会话 * - 写信(预填收件人为该三维地址) * - 归档(Agent 侧会话归档 + 邮箱界面移除) */ export default function ContactPanel() { const contacts = useContactStore(s => s.contacts); const archivedContacts = useContactStore(s => s.archivedContacts); const showArchived = useContactStore(s => s.showArchived); const loading = useContactStore(s => s.loading); const error = useContactStore(s => s.error); const pendingArchive = useContactStore(s => s.pendingArchive); const fetchContacts = useContactStore(s => s.fetchContacts); const toggleArchivedView = useContactStore(s => s.toggleArchivedView); const requestArchive = useContactStore(s => s.requestArchive); const cancelArchive = useContactStore(s => s.cancelArchive); const archive = useContactStore(s => s.archive); const view = useContactStore(s => s.view); const setView = useContactStore(s => s.setView); const selectSession = useSessionStore(s => s.selectSession); const currentSession = useSessionStore(s => s.currentSession); const clearCurrentMail = useMailStore(s => s.clearCurrentMail); const startCompose = useUIStore(s => s.startCompose); const cancelCompose = useUIStore(s => s.cancelCompose); const showDetail = useUIStore(s => s.showDetail); useEffect(() => { fetchContacts(); }, []); const open = (c: Contact) => { cancelCompose(); clearCurrentMail(); selectSession(c.session_id); showDetail(); // 窄屏下切到会话内容栏 }; return (

{view === 'card' ? '工作列表' : '联系人'}

{contacts.length}
{/* 视图切换:列表答「跟谁在聊」,卡片答「在聊什么、进展如何」 */}
{error &&

{error}

}
{loading && contacts.length === 0 && (

加载中

)} {contacts.map(c => // 归档确认态两种视图共用同一个确认框:那是个破坏性操作, // 换个视图就换套确认 UI 只会让人对「点了什么」更没底 pendingArchive === c.address ? ( archive(c)} /> ) : view === 'card' ? ( open(c)} onCompose={() => startCompose({ to: c.address })} onArchive={() => requestArchive(c.address)} /> ) : ( open(c)} onCompose={() => startCompose({ to: c.address })} onRequestArchive={() => requestArchive(c.address)} /> ) )} {!loading && contacts.length === 0 && (

{view === 'card' ? '暂无进行中的工作,发一封邮件即可开始' : '暂无联系人,发一封邮件即可建立'}

)} {showArchived && (

已归档 {archivedContacts.length}

{archivedContacts.map(c => (

{c.address}

{c.mail_count} 封 · 已归档

))} {archivedContacts.length === 0 && (

无归档会话

)}
)}
); } /** * 归档确认框。 * * 列表视图与卡片视图共用:归档是破坏性操作,换个视图就换套确认 UI * 只会让人对「自己点了什么」更没底。 */ function ArchiveConfirm({ contact, onCancel, onConfirm }: { contact: Contact; onCancel: () => void; onConfirm: () => void; }) { return (

归档 {contact.address}?

对应 Agent 的 session 将被归档,此列表与邮箱界面同时移除

); } function ContactRow({ contact, active, onOpen, onCompose, onRequestArchive }: { contact: Contact; active: boolean; onOpen: () => void; onCompose: () => void; onRequestArchive: () => void; }) { const time = new Date(contact.last_activity).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); return (
); }