diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 69f33807e5..21b9bc22c1 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -39,6 +39,7 @@ import PermissionSettingsView from './components/settings/permission/PermissionS import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView'; import RecipesView from './components/recipes/RecipesView'; import RecipeEditor from './components/recipes/RecipeEditor'; +import PeersView from './components/peers/PeersView'; import { createNavigationHandler, View, ViewOptions } from './utils/navigationUtils'; import { AgentState, @@ -307,6 +308,11 @@ const ExtensionsRoute = () => { ); }; +const PeersRoute = () => { + const navigate = useNavigate(); + return navigate('/')} />; +}; + export function AppInner() { const [fatalError, setFatalError] = useState(null); const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false); @@ -598,9 +604,10 @@ export function AppInner() { /> } /> } /> + } /> } /> } /> - } /> + } /> } /> void; +} + +// Mock data for development +const mockPeers: Peer[] = [ + { + id: '1', + username: 'alice_dev', + displayName: 'Alice Johnson', + status: 'online', + lastSeen: new Date(), + }, + { + id: '2', + username: 'bob_designer', + displayName: 'Bob Smith', + status: 'away', + lastSeen: new Date(Date.now() - 1000 * 60 * 15), // 15 minutes ago + }, + { + id: '3', + username: 'charlie_pm', + displayName: 'Charlie Brown', + status: 'busy', + lastSeen: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago + }, + { + id: '4', + username: 'diana_qa', + displayName: 'Diana Prince', + status: 'offline', + lastSeen: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago + }, +]; + +const mockPendingRequests: PendingRequest[] = [ + { + id: '1', + username: 'eve_new', + displayName: 'Eve Wilson', + type: 'incoming', + timestamp: new Date(Date.now() - 1000 * 60 * 10), // 10 minutes ago + }, + { + id: '2', + username: 'frank_dev', + displayName: 'Frank Miller', + type: 'outgoing', + timestamp: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago + }, +]; + +const StatusIndicator: React.FC<{ status: Peer['status'] }> = ({ status }) => { + const statusConfig = { + online: { color: 'bg-green-500', label: 'Online' }, + away: { color: 'bg-yellow-500', label: 'Away' }, + busy: { color: 'bg-red-500', label: 'Busy' }, + offline: { color: 'bg-gray-400', label: 'Offline' }, + }; + + const config = statusConfig[status]; + + return ( +
+
+ {config.label} +
+ ); +}; + +const PeerCard: React.FC<{ + peer: Peer; + onStartChat: (peer: Peer) => void; + onRemovePeer: (peer: Peer) => void; +}> = ({ peer, onStartChat, onRemovePeer }) => { + const [showActions, setShowActions] = useState(false); + + const formatLastSeen = (date: Date) => { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const minutes = Math.floor(diff / (1000 * 60)); + const hours = Math.floor(diff / (1000 * 60 * 60)); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + return `${days}d ago`; + }; + + return ( + + {/* Avatar and Info */} +
+
+
+ + {peer.displayName.charAt(0).toUpperCase()} + +
+ {/* Status dot */} +
+
+ +
+

{peer.displayName}

+

@{peer.username}

+
+ +
+ {peer.status !== 'online' && peer.lastSeen && ( +

+ Last seen {formatLastSeen(peer.lastSeen)} +

+ )} +
+ + {/* Actions */} +
+ onStartChat(peer)} + className="p-2 rounded-lg bg-background-accent text-text-on-accent hover:bg-background-accent/80 transition-colors" + title="Start AI chat session" + > + + + +
+ setShowActions(!showActions)} + className="p-2 rounded-lg hover:bg-background-medium transition-colors opacity-0 group-hover:opacity-100" + > + + + + + {showActions && ( + + + +
+ + + )} + +
+
+
+ + ); +}; + +const PendingRequestCard: React.FC<{ + request: PendingRequest; + onAccept: (request: PendingRequest) => void; + onDecline: (request: PendingRequest) => void; + onCancel: (request: PendingRequest) => void; +}> = ({ request, onAccept, onDecline, onCancel }) => { + const formatTimestamp = (date: Date) => { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const minutes = Math.floor(diff / (1000 * 60)); + const hours = Math.floor(diff / (1000 * 60 * 60)); + + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + return `${hours}h ago`; + }; + + return ( + +
+
+ + {request.displayName.charAt(0).toUpperCase()} + +
+ +
+

{request.displayName}

+

@{request.username}

+

+ {request.type === 'incoming' ? 'Wants to be friends' : 'Friend request sent'} • {formatTimestamp(request.timestamp)} +

+
+ +
+ {request.type === 'incoming' ? ( + <> + onAccept(request)} + className="p-2 rounded-lg bg-green-500 text-white hover:bg-green-600 transition-colors" + title="Accept request" + > + + + onDecline(request)} + className="p-2 rounded-lg bg-red-500 text-white hover:bg-red-600 transition-colors" + title="Decline request" + > + + + + ) : ( + onCancel(request)} + className="p-2 rounded-lg hover:bg-background-medium transition-colors" + title="Cancel request" + > + + + )} +
+
+
+ ); +}; + +const AddFriendModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onSendRequest: (username: string) => void; +}> = ({ isOpen, onClose, onSendRequest }) => { + const [username, setUsername] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim()) return; + + setIsLoading(true); + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + onSendRequest(username.trim()); + setUsername(''); + setIsLoading(false); + onClose(); + }; + + if (!isOpen) return null; + + return ( + + e.stopPropagation()} + > +
+

Add Friend

+ +
+ +
+
+ + setUsername(e.target.value)} + placeholder="Enter username (e.g., alice_dev)" + className="w-full px-4 py-3 rounded-lg border border-border-default bg-background-muted focus:outline-none focus:ring-2 focus:ring-background-accent" + disabled={isLoading} + /> +

+ Enter the exact username of the person you want to add as a friend. +

+
+ +
+ + +
+
+
+
+ ); +}; + +const PeersView: React.FC = ({ onClose }) => { + const [peers, setPeers] = useState(mockPeers); + const [pendingRequests, setPendingRequests] = useState(mockPendingRequests); + const [searchQuery, setSearchQuery] = useState(''); + const [showAddFriendModal, setShowAddFriendModal] = useState(false); + const [activeTab, setActiveTab] = useState<'friends' | 'requests'>('friends'); + + // Filter peers based on search query + const filteredPeers = peers.filter(peer => + peer.displayName.toLowerCase().includes(searchQuery.toLowerCase()) || + peer.username.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Group peers by status + const onlinePeers = filteredPeers.filter(peer => peer.status === 'online'); + const offlinePeers = filteredPeers.filter(peer => peer.status !== 'online'); + + const handleStartChat = (peer: Peer) => { + // TODO: Implement P2P chat session creation + console.log('Starting chat with:', peer); + // This will eventually create a shared AI chat session + }; + + const handleRemovePeer = (peer: Peer) => { + setPeers(prev => prev.filter(p => p.id !== peer.id)); + }; + + const handleAcceptRequest = (request: PendingRequest) => { + // Add to friends list + const newPeer: Peer = { + id: Date.now().toString(), + username: request.username, + displayName: request.displayName, + status: 'online', // Assume they're online when accepted + lastSeen: new Date(), + }; + setPeers(prev => [...prev, newPeer]); + + // Remove from pending requests + setPendingRequests(prev => prev.filter(r => r.id !== request.id)); + }; + + const handleDeclineRequest = (request: PendingRequest) => { + setPendingRequests(prev => prev.filter(r => r.id !== request.id)); + }; + + const handleCancelRequest = (request: PendingRequest) => { + setPendingRequests(prev => prev.filter(r => r.id !== request.id)); + }; + + const handleSendFriendRequest = (username: string) => { + const newRequest: PendingRequest = { + id: Date.now().toString(), + username, + displayName: username.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()), + type: 'outgoing', + timestamp: new Date(), + }; + setPendingRequests(prev => [...prev, newRequest]); + }; + + const incomingRequests = pendingRequests.filter(r => r.type === 'incoming'); + const outgoingRequests = pendingRequests.filter(r => r.type === 'outgoing'); + + return ( +
+ {/* Header */} +
+
+ +

Friends

+ + {peers.length} + +
+ +
+ setShowAddFriendModal(true)} + className="flex items-center gap-2 px-4 py-2 rounded-lg bg-background-accent text-text-on-accent hover:bg-background-accent/80 transition-colors" + > + + Add Friend + + + +
+
+ + {/* Tabs */} +
+ + +
+ + {/* Content */} +
+ {activeTab === 'friends' ? ( +
+ {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-3 rounded-lg border border-border-default bg-background-muted focus:outline-none focus:ring-2 focus:ring-background-accent" + /> +
+
+ + {/* Friends List */} +
+ {filteredPeers.length === 0 ? ( +
+ +

+ {searchQuery ? 'No friends found' : 'No friends yet'} +

+

+ {searchQuery + ? 'Try adjusting your search terms' + : 'Add friends to start collaborative AI chat sessions' + } +

+ {!searchQuery && ( + + )} +
+ ) : ( + <> + {/* Online Friends */} + {onlinePeers.length > 0 && ( +
+

+ + Online ({onlinePeers.length}) +

+
+ {onlinePeers.map((peer) => ( + + ))} +
+
+ )} + + {/* Offline Friends */} + {offlinePeers.length > 0 && ( +
+

+ + Offline ({offlinePeers.length}) +

+
+ {offlinePeers.map((peer) => ( + + ))} +
+
+ )} + + )} +
+
+ ) : ( + /* Requests Tab */ +
+ {pendingRequests.length === 0 ? ( +
+ +

No pending requests

+

+ Friend requests will appear here when you receive them or send them. +

+
+ ) : ( + <> + {/* Incoming Requests */} + {incomingRequests.length > 0 && ( +
+

+ + Incoming Requests ({incomingRequests.length}) +

+
+ {incomingRequests.map((request) => ( + + ))} +
+
+ )} + + {/* Outgoing Requests */} + {outgoingRequests.length > 0 && ( +
+

+ + Sent Requests ({outgoingRequests.length}) +

+
+ {outgoingRequests.map((request) => ( + + ))} +
+
+ )} + + )} +
+ )} +
+ + {/* Add Friend Modal */} + + {showAddFriendModal && ( + setShowAddFriendModal(false)} + onSendRequest={handleSendFriendRequest} + /> + )} + +
+ ); +}; + +export default PeersView; diff --git a/ui/desktop/src/components/peers/index.ts b/ui/desktop/src/components/peers/index.ts new file mode 100644 index 0000000000..b3eb4b18bd --- /dev/null +++ b/ui/desktop/src/components/peers/index.ts @@ -0,0 +1 @@ +export { default as PeersView } from './PeersView';