Emissary List Minimal Component

Overview

The Emissary List Minimal Component (src/components/EmissaryListMinimal.tsx) provides a compact sidebar interface for displaying recent AI assistant conversations. It’s optimized for sidebar integration with minimal space usage while maintaining full functionality for navigation and conversation access.

Key Features

  • Compact design: Optimized for narrow sidebar spaces
  • SidebarMenu integration: Built using the application’s sidebar component system
  • Responsive behavior: Adapts to different sidebar widths
  • Minimal visual footprint: Clean, space-efficient layout

Recent Conversations

  • Last accessed sorting: Shows assistants ordered by recent activity
  • Message preview: Displays latest message content for each assistant
  • Relative timestamps: Human-readable time indicators
  • Expandable list: Collapsible interface for managing large lists

Smart Features

  • Firebase integration: Real-time message fetching and display
  • Loading states: Graceful loading indicators
  • Empty states: Helpful messaging when no conversations exist
  • Tooltip support: Enhanced information on hover

Component Structure

Props Interface

interface EmissaryListMinimalProps {
  userId?: string;
  maxItems?: number;
  isCollapsible?: boolean;
}

Configuration Options

  • userId: User identifier for fetching personal assistants
  • maxItems: Maximum number of assistants to display initially (default: 5)
  • isCollapsible: Whether to show expand/collapse functionality (default: true)

Core Functionality

Assistant Loading and Sorting

useEffect(() => {
  const loadRecentAssistants = async () => {
    if (!userId) {
      setIsLoading(false);
      return;
    }

    setIsLoading(true);
    try {
      // Get user's assistants
      const userAssistants = await FirebaseService.getUserAssistants(userId);
      
      // Sort by last accessed date, most recent first
      const sortedAssistants = userAssistants
        .sort((a, b) => {
          const aTime = a.lastAccessedAt ? new Date(a.lastAccessedAt).getTime() : 0;
          const bTime = b.lastAccessedAt ? new Date(b.lastAccessedAt).getTime() : 0;
          return bTime - aTime;
        })
        .map(assistant => ({
          // Transform to AssistantDisplay format
          assistantId: assistant.assistantId,
          name: assistant.name,
          avatar: assistant.avatar,
          shareUrl: `/assistant/${assistant.assistantId}`,
          lastAccessedAt: assistant.lastAccessedAt ? new Date(assistant.lastAccessedAt) : undefined,
          // ... other properties
        }));

      setAssistants(sortedAssistants);
      // Fetch last messages for each assistant
      await fetchLastMessages(sortedAssistants);
    } catch (error) {
      console.error('Error loading recent assistants:', error);
      setAssistants([]);
    } finally {
      setIsLoading(false);
    }
  };

  loadRecentAssistants();
}, [userId]);

Message Fetching

// Fetch last message for each assistant
const messagePromises = sortedAssistants.map(async (assistant) => {
  try {
    const messages = await FirebaseService.getMessages(assistant.assistantId);
    if (messages.length > 0) {
      // Get the last message (most recent)
      const lastMessage = messages[messages.length - 1];
      return { assistantId: assistant.assistantId, message: lastMessage };
    }
    return null;
  } catch (error) {
    console.error(`Error fetching messages for assistant ${assistant.assistantId}:`, error);
    return null;
  }
});

const messageResults = await Promise.all(messagePromises);
const messageMap: Record<string, Message> = {};

messageResults.forEach(result => {
  if (result) {
    messageMap[result.assistantId] = result.message;
  }
});

setLastMessages(messageMap);

UI Implementation

return (
  <TooltipProvider>
    <SidebarGroup>
      <SidebarGroupContent>
        <SidebarMenu>
          {displayedAssistants.map((assistant) => {
            const lastMessage = lastMessages[assistant.assistantId];
            const displayContent = lastMessage 
              ? truncateMessage(lastMessage.content) 
              : 'No messages yet';
            const tooltipContent = lastMessage?.content 
              ? truncateMessage(lastMessage.content, 200) 
              : 'Start a conversation';

            return (
              <SidebarMenuItem key={assistant.assistantId} className="my-0.5">
                <Tooltip>
                  <TooltipTrigger asChild>
                    <SidebarMenuButton asChild className="h-auto py-2">
                      <Link href={assistant.shareUrl || ''} className="flex items-center gap-2 w-full">
                        {/* Assistant content */}
                      </Link>
                    </SidebarMenuButton>
                  </TooltipTrigger>
                  <TooltipContent side="right" className="max-w-xs">
                    {/* Tooltip content */}
                  </TooltipContent>
                </Tooltip>
              </SidebarMenuItem>
            );
          })}
        </SidebarMenu>
      </SidebarGroupContent>
    </SidebarGroup>
  </TooltipProvider>
);

Assistant Item Layout

<Link href={assistant.shareUrl || ''} className="flex items-center gap-2 w-full">
  <Avatar className="h-6 w-6 shrink-0">
    <AvatarImage src={assistant.avatar} alt={assistant.name} />
    <AvatarFallback>{assistant.name[0]}</AvatarFallback>
  </Avatar>
  <div className="flex-1 min-w-0">
    <div className="text-xs text-muted-foreground truncate">
      {displayContent}
    </div>
  </div>
  <div className="flex items-center gap-2 shrink-0">
    <span className="text-xs text-muted-foreground">
      <RelativeTime date={assistant.lastAccessedAt} showTooltip={false} />
    </span>
    <MessageCircle className="h-3 w-3 text-muted-foreground" />
  </div>
</Link>

Content Management

Message Truncation

const truncateMessage = (content: string, maxLength: number = 30) => {
  if (content.length <= maxLength) return content;
  return content.substring(0, maxLength).trim() + '...';
};

Expand/Collapse Functionality

const displayedAssistants = isExpanded ? assistants : assistants.slice(0, maxItems);
const hasMore = assistants.length > maxItems;

{isCollapsible && hasMore && (
  <Button
    variant="ghost"
    size="sm"
    className="w-full mt-2 justify-center"
    onClick={() => setIsExpanded(!isExpanded)}
  >
    {isExpanded ? (
      <>
        <ChevronUp className="h-4 w-4 mr-1" />
        Show Less
      </>
    ) : (
      <>
        <ChevronDown className="h-4 w-4 mr-1" />
        Show {assistants.length - maxItems} More
      </>
    )}
  </Button>
)}

Tooltip Enhancement

Rich Tooltip Content

<TooltipContent side="right" className="max-w-xs">
  <div className="space-y-1">
    <div className="flex items-center gap-2">
      <Avatar className="h-6 w-6 shrink-0">
        <AvatarImage src={assistant.avatar} alt={assistant.name} />
        <AvatarFallback>{assistant.name[0]}</AvatarFallback>
      </Avatar>
      <p className="font-medium">{assistant.name}</p>
    </div>
    <p className="text-sm text-muted-foreground">{tooltipContent}</p>
  </div>
</TooltipContent>

Tooltip Features:

  • Assistant name: Shows full assistant name
  • Avatar: Displays assistant avatar for visual recognition
  • Extended preview: Shows longer message preview (200 characters)
  • Contextual content: Different content for conversations vs. new assistants

State Management

Loading States

if (isLoading) {
  return (
    <SidebarGroup>
      <SidebarGroupContent>
        <div className="p-4 text-center text-sm text-muted-foreground">
          Loading...
        </div>
      </SidebarGroupContent>
    </SidebarGroup>
  );
}

Empty States

if (assistants.length === 0) {
  return (
    <SidebarGroup>
      <SidebarGroupContent>
        <div className="p-4 text-center text-sm text-muted-foreground">
          No conversations yet
        </div>
      </SidebarGroupContent>
    </SidebarGroup>
  );
}

Error Handling

try {
  const userAssistants = await FirebaseService.getUserAssistants(userId);
  // Process assistants
} catch (error) {
  console.error('Error loading recent assistants:', error);
  toast({
    title: "Error Loading Assistants",
    description: "Failed to load recent assistants",
    variant: "destructive"
  });
  setAssistants([]);
}

Integration Examples

import { EmissaryListMinimal } from '@/components/EmissaryListMinimal';

function AppSidebar({ currentUser }) {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Recent Conversations</SidebarGroupLabel>
          <EmissaryListMinimal 
            userId={currentUser?.uid}
            maxItems={5}
            isCollapsible={true}
          />
        </SidebarGroup>
        
        {/* Other sidebar content */}
      </SidebarContent>
    </Sidebar>
  );
}

Custom Configuration

// Show more items initially
<EmissaryListMinimal 
  userId={userId}
  maxItems={10}
  isCollapsible={true}
/>

// Fixed list without expansion
<EmissaryListMinimal 
  userId={userId}
  maxItems={3}
  isCollapsible={false}
/>

// Guest mode (no user ID)
<EmissaryListMinimal 
  userId={undefined}
  maxItems={5}
/>

Performance Optimizations

Efficient Data Loading

// Only fetch when user ID is available
if (!userId) {
  setIsLoading(false);
  return;
}

// Add delay to ensure Firebase initialization
await new Promise(resolve => setTimeout(resolve, 100));

Parallel Message Fetching

// Fetch messages for all assistants in parallel
const messagePromises = sortedAssistants.map(async (assistant) => {
  try {
    const messages = await FirebaseService.getMessages(assistant.assistantId);
    return messages.length > 0 ? { assistantId: assistant.assistantId, message: messages[messages.length - 1] } : null;
  } catch (error) {
    return null;
  }
});

const messageResults = await Promise.all(messagePromises);

Memory Efficiency

  • Lazy loading: Only loads data when user ID is provided
  • Selective rendering: Only renders displayed items
  • Cleanup: Proper cleanup of async operations

Accessibility Features

Keyboard Navigation

  • Tab support: Full keyboard navigation through items
  • Link activation: Standard link activation with Enter key
  • Focus management: Proper focus indicators

Screen Reader Support

  • Semantic HTML: Proper use of navigation and list elements
  • Alt text: Descriptive alt text for avatars
  • ARIA labels: Appropriate labeling for interactive elements

Visual Accessibility

  • High contrast: Clear visual hierarchy and contrast
  • Icon support: Meaningful icons with text labels
  • Responsive text: Scalable text sizing

Future Enhancements

Planned Features

  • Drag and drop: Reorder conversations by importance
  • Context menu: Right-click actions for conversations
  • Search integration: Quick search through conversations
  • Keyboard shortcuts: Hotkeys for quick navigation

Performance Improvements

  • Virtual scrolling: Handle large numbers of conversations
  • Caching: Cache recent conversation data
  • Incremental loading: Load conversations on demand
  • Real-time updates: Live updates when new messages arrive

Troubleshooting

Common Issues

Conversations not loading

// Check userId is valid
// Verify Firebase authentication
// Check network connectivity
// Review console for error messages

Messages not displaying

// Verify Firebase permissions
// Check message data structure
// Ensure proper async handling
// Test with different assistants

Tooltip not showing

// Verify TooltipProvider is wrapping component
// Check tooltip positioning
// Ensure proper hover events
// Test on different screen sizes