/** * Channels Page * Manage messaging channel connections */ import { useState, useEffect } from 'react'; import { Plus, Radio, RefreshCw, Settings, Trash2, Power, PowerOff, QrCode, Loader2, X, ExternalLink, Copy, Check, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { useChannelsStore } from '@/stores/channels'; import { useGatewayStore } from '@/stores/gateway'; import { StatusBadge, type Status } from '@/components/common/StatusBadge'; import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { CHANNEL_ICONS, CHANNEL_NAMES, type ChannelType, type Channel } from '@/types/channel'; import { toast } from 'sonner'; // Channel type info with connection instructions const channelInfo: Record = { whatsapp: { description: 'Connect WhatsApp by scanning a QR code', connectionType: 'qr', instructions: [ 'Open WhatsApp on your phone', 'Go to Settings > Linked Devices', 'Tap "Link a Device"', 'Scan the QR code below', ], docsUrl: 'https://faq.whatsapp.com/1317564962315842', }, telegram: { description: 'Connect Telegram using a bot token', connectionType: 'token', instructions: [ 'Open Telegram and search for @BotFather', 'Send /newbot and follow the instructions', 'Copy the bot token provided', 'Paste it below', ], tokenLabel: 'Bot Token', docsUrl: 'https://core.telegram.org/bots#how-do-i-create-a-bot', }, discord: { description: 'Connect Discord using a bot token', connectionType: 'token', instructions: [ 'Go to Discord Developer Portal', 'Create a new Application', 'Go to Bot section and create a bot', 'Copy the bot token', ], tokenLabel: 'Bot Token', docsUrl: 'https://discord.com/developers/applications', }, slack: { description: 'Connect Slack via OAuth', connectionType: 'token', instructions: [ 'Go to Slack API apps page', 'Create a new app', 'Configure OAuth scopes', 'Install to workspace and copy the token', ], tokenLabel: 'Bot Token (xoxb-...)', docsUrl: 'https://api.slack.com/apps', }, wechat: { description: 'Connect WeChat by scanning a QR code', connectionType: 'qr', instructions: [ 'Open WeChat on your phone', 'Scan the QR code below', 'Confirm login on your phone', ], }, }; export function Channels() { const { channels, loading, error, fetchChannels, connectChannel, disconnectChannel, deleteChannel } = useChannelsStore(); const gatewayStatus = useGatewayStore((state) => state.status); const [showAddDialog, setShowAddDialog] = useState(false); const [selectedChannelType, setSelectedChannelType] = useState(null); const [connectingChannelId, setConnectingChannelId] = useState(null); // Fetch channels on mount useEffect(() => { fetchChannels(); }, [fetchChannels]); // Supported channel types for adding const supportedTypes: ChannelType[] = ['whatsapp', 'telegram', 'discord', 'slack']; // Connected/disconnected channel counts const connectedCount = channels.filter((c) => c.status === 'connected').length; if (loading) { return (
); } return (
{/* Header */}

Channels

Connect and manage your messaging channels

{/* Stats */}

{channels.length}

Total Channels

{connectedCount}

Connected

{channels.length - connectedCount}

Disconnected

{/* Gateway Warning */} {gatewayStatus.state !== 'running' && (
Gateway is not running. Channels cannot connect without an active Gateway. )} {/* Error Display */} {error && ( {error} )} {/* Channels Grid */} {channels.length === 0 ? (

No channels configured

Connect a messaging channel to start using ClawX

) : (
{channels.map((channel) => ( { setConnectingChannelId(channel.id); connectChannel(channel.id); }} onDisconnect={() => disconnectChannel(channel.id)} onDelete={() => { if (confirm('Are you sure you want to delete this channel?')) { deleteChannel(channel.id); } }} isConnecting={connectingChannelId === channel.id} /> ))}
)} {/* Add Channel Section */} Supported Channels Click on a channel type to add it
{supportedTypes.map((type) => ( ))}
{/* Add Channel Dialog */} {showAddDialog && ( { setShowAddDialog(false); setSelectedChannelType(null); }} supportedTypes={supportedTypes} /> )}
); } // ==================== Channel Card Component ==================== interface ChannelCardProps { channel: Channel; onConnect: () => void; onDisconnect: () => void; onDelete: () => void; isConnecting: boolean; } function ChannelCard({ channel, onConnect, onDisconnect, onDelete, isConnecting }: ChannelCardProps) { return (
{CHANNEL_ICONS[channel.type]}
{channel.name} {CHANNEL_NAMES[channel.type]}
{channel.lastActivity && (

Last activity: {new Date(channel.lastActivity).toLocaleString()}

)} {channel.error && (

{channel.error}

)}
{channel.status === 'connected' ? ( ) : ( )}
); } // ==================== Add Channel Dialog ==================== interface AddChannelDialogProps { selectedType: ChannelType | null; onSelectType: (type: ChannelType | null) => void; onClose: () => void; supportedTypes: ChannelType[]; } function AddChannelDialog({ selectedType, onSelectType, onClose, supportedTypes }: AddChannelDialogProps) { const { addChannel } = useChannelsStore(); const [channelName, setChannelName] = useState(''); const [token, setToken] = useState(''); const [connecting, setConnecting] = useState(false); const [qrCode, setQrCode] = useState(null); const [copied, setCopied] = useState(false); const info = selectedType ? channelInfo[selectedType] : null; const handleConnect = async () => { if (!selectedType) return; setConnecting(true); try { // For QR-based channels, we'd request a QR code from the gateway if (info?.connectionType === 'qr') { // Simulate QR code generation await new Promise((resolve) => setTimeout(resolve, 1500)); setQrCode('placeholder-qr'); } else { // For token-based, add the channel with the token await addChannel({ type: selectedType, name: channelName || CHANNEL_NAMES[selectedType], token: token || undefined, }); toast.success(`${CHANNEL_NAMES[selectedType]} channel added`); onClose(); } } catch (error) { toast.error(`Failed to add channel: ${error}`); } finally { setConnecting(false); } }; const copyToken = () => { navigator.clipboard.writeText(token); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return (
{selectedType ? `Connect ${CHANNEL_NAMES[selectedType]}` : 'Add Channel'} {info?.description || 'Select a messaging channel to connect'}
{!selectedType ? ( // Channel type selection
{supportedTypes.map((type) => ( ))}
) : qrCode ? ( // QR Code display

Scan this QR code with {CHANNEL_NAMES[selectedType]} to connect

) : ( // Connection form
{/* Instructions */}

How to connect:

    {info?.instructions.map((instruction, i) => (
  1. {instruction}
  2. ))}
{info?.docsUrl && ( )}
{/* Channel name */}
setChannelName(e.target.value)} />
{/* Token input for token-based channels */} {info?.connectionType === 'token' && (
setToken(e.target.value)} /> {token && ( )}
)}
)}
); } export default Channels;