All files / src/components ConversationList.tsx

0% Statements 0/93
0% Branches 0/1
0% Functions 0/1
0% Lines 0/93

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                                                                                                                                                                                                                                               
import { useChatStore } from '../store/chatStore';
import { MessageSquare, Trash2, SquarePen } from 'lucide-react';
import { EmptyConversations, ConversationListSkeleton } from './ui';
 
export function ConversationList() {
  const {
    conversations, currentConversationId, messages, agents, currentAgent,
    newConversation, switchConversation, deleteConversation,
    isLoading,
  } = useChatStore();
 
  const hasActiveChat = messages.length > 0;
 
  // Show skeleton during initial load
  if (isLoading && conversations.length === 0 && !hasActiveChat) {
    return <ConversationListSkeleton count={4} />;
  }
 
  return (
    <div className="h-full flex flex-col">
      {/* Header */}
      <div className="flex items-center justify-between px-3 py-2 border-b border-gray-200">
        <span className="text-xs font-medium text-gray-500">对话历史</span>
        <button
          onClick={newConversation}
          className="p-1 text-gray-400 hover:text-orange-500 rounded"
          title="新对话"
        >
          <SquarePen className="w-4 h-4" />
        </button>
      </div>
 
      <div className="flex-1 overflow-y-auto custom-scrollbar">
        {/* Current active chat (unsaved) */}
        {hasActiveChat && !currentConversationId && (
          <div className="flex items-center gap-3 px-3 py-3 bg-orange-50 border-b border-orange-100 cursor-default">
            <div className="w-7 h-7 bg-orange-500 rounded-lg flex items-center justify-center text-white flex-shrink-0">
              <MessageSquare className="w-3.5 h-3.5" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="text-xs font-medium text-orange-700 truncate">当前对话</div>
              <div className="text-[11px] text-orange-500 truncate">
                {messages.filter(m => m.role === 'user').length} 条消息 · {currentAgent?.name || 'ZCLAW'}
              </div>
            </div>
          </div>
        )}
 
        {/* Saved conversations */}
        {conversations.map((conv) => {
          const isActive = conv.id === currentConversationId;
          const msgCount = conv.messages.filter(m => m.role === 'user').length;
          const timeStr = formatTime(conv.updatedAt);
          const agentName = conv.agentId
            ? agents.find((agent) => agent.id === conv.agentId)?.name || conv.agentId
            : 'ZCLAW';
 
          return (
            <div
              key={conv.id}
              onClick={() => switchConversation(conv.id)}
              className={`group flex items-center gap-3 px-3 py-3 cursor-pointer border-b border-gray-50 transition-colors ${
                isActive ? 'bg-orange-50' : 'hover:bg-gray-100'
              }`}
            >
              <div className={`w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 ${
                isActive ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-500'
              }`}>
                <MessageSquare className="w-3.5 h-3.5" />
              </div>
              <div className="flex-1 min-w-0">
                <div className={`text-xs font-medium truncate ${isActive ? 'text-orange-700' : 'text-gray-900'}`}>
                  {conv.title}
                </div>
                <div className="text-[11px] text-gray-400 truncate">
                  {msgCount} 条消息 · {agentName} · {timeStr}
                </div>
              </div>
              <button
                onClick={(e) => {
                  e.stopPropagation();
                  if (confirm('删除该对话?')) {
                    deleteConversation(conv.id);
                  }
                }}
                className="opacity-0 group-hover:opacity-100 p-1 text-gray-300 hover:text-red-500 transition-opacity"
                title="删除"
              >
                <Trash2 className="w-3 h-3" />
              </button>
            </div>
          );
        })}
 
        {conversations.length === 0 && !hasActiveChat && (
          <EmptyConversations size="sm" className="h-auto" />
        )}
      </div>
    </div>
  );
}
 
function formatTime(date: Date): string {
  const now = new Date();
  const d = new Date(date);
  const diffMs = now.getTime() - d.getTime();
  const diffMin = Math.floor(diffMs / 60000);
  
  if (diffMin < 1) return '刚刚';
  if (diffMin < 60) return `${diffMin} 分钟前`;
  
  const diffHr = Math.floor(diffMin / 60);
  if (diffHr < 24) return `${diffHr} 小时前`;
  
  const diffDay = Math.floor(diffHr / 24);
  if (diffDay < 7) return `${diffDay} 天前`;
  
  return `${d.getMonth() + 1}/${d.getDate()}`;
}