chatroom-sheet.tsx
1 'use client' 2 3 import { useState, useEffect } from 'react' 4 import { useChatroomStore } from '@/stores/use-chatroom-store' 5 import { useAppStore } from '@/stores/use-app-store' 6 import { BottomSheet } from '@/components/shared/bottom-sheet' 7 import { ConfirmDialog } from '@/components/shared/confirm-dialog' 8 import { toast } from 'sonner' 9 import { AgentAvatar } from '@/components/agents/agent-avatar' 10 import type { Agent } from '@/types' 11 import { CheckIcon } from '@/components/shared/check-icon' 12 13 export function ChatroomSheet() { 14 const open = useChatroomStore((s) => s.chatroomSheetOpen) 15 const editingId = useChatroomStore((s) => s.editingChatroomId) 16 const chatrooms = useChatroomStore((s) => s.chatrooms) 17 const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen) 18 const createChatroom = useChatroomStore((s) => s.createChatroom) 19 const updateChatroom = useChatroomStore((s) => s.updateChatroom) 20 const deleteChatroom = useChatroomStore((s) => s.deleteChatroom) 21 const setCurrentChatroom = useChatroomStore((s) => s.setCurrentChatroom) 22 const agents = useAppStore((s) => s.agents) 23 24 const [name, setName] = useState('') 25 const [description, setDescription] = useState('') 26 const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([]) 27 const [chatMode, setChatMode] = useState<'sequential' | 'parallel'>('sequential') 28 const [autoAddress, setAutoAddress] = useState(false) 29 const [routingGuidance, setRoutingGuidance] = useState('') 30 const [saving, setSaving] = useState(false) 31 const [confirmDelete, setConfirmDelete] = useState(false) 32 33 const editing = editingId ? chatrooms[editingId] : null 34 35 useEffect(() => { 36 if (editing) { 37 setName(editing.name) 38 setDescription(editing.description || '') 39 setSelectedAgentIds([...editing.agentIds]) 40 setChatMode(editing.chatMode || 'sequential') 41 setAutoAddress(editing.autoAddress || false) 42 setRoutingGuidance(editing.routingGuidance || '') 43 } else { 44 setName('') 45 setDescription('') 46 setSelectedAgentIds([]) 47 setChatMode('sequential') 48 setAutoAddress(false) 49 setRoutingGuidance('') 50 } 51 setConfirmDelete(false) 52 }, [editing, open]) 53 54 const handleSave = async () => { 55 if (!name.trim() || saving) return 56 if (selectedAgentIds.length === 0) { 57 toast.error('Select at least one chatroom member.') 58 return 59 } 60 setSaving(true) 61 try { 62 const payload = { 63 name, 64 description, 65 agentIds: selectedAgentIds, 66 chatMode, 67 autoAddress, 68 routingGuidance: routingGuidance.trim() || null, 69 } 70 if (editing) { 71 await updateChatroom(editing.id, payload) 72 toast.success('Chatroom updated successfully') 73 } else { 74 const chatroom = await createChatroom(payload) 75 setCurrentChatroom(chatroom.id) 76 toast.success('Chatroom created successfully') 77 } 78 setChatroomSheetOpen(false) 79 } catch (err: unknown) { 80 toast.error(err instanceof Error ? err.message : 'Failed to save chatroom') 81 } finally { 82 setSaving(false) 83 } 84 } 85 86 const handleDelete = async () => { 87 if (!editing || saving) return 88 setSaving(true) 89 try { 90 await deleteChatroom(editing.id) 91 toast.success('Chatroom deleted') 92 setConfirmDelete(false) 93 setChatroomSheetOpen(false) 94 } catch (err: unknown) { 95 toast.error(err instanceof Error ? err.message : 'Failed to delete chatroom') 96 } finally { 97 setSaving(false) 98 } 99 } 100 101 const toggleAgent = (agentId: string) => { 102 setSelectedAgentIds((prev) => 103 prev.includes(agentId) ? prev.filter((id) => id !== agentId) : [...prev, agentId] 104 ) 105 } 106 107 const agentList = Object.values(agents).filter((a: Agent) => !a.trashedAt) as Agent[] 108 109 return ( 110 <BottomSheet open={open} onClose={() => setChatroomSheetOpen(false)}> 111 <div className="p-6 max-w-[560px] mx-auto"> 112 <h2 className="font-display text-[18px] font-700 text-text mb-4"> 113 {editing ? 'Edit Chatroom' : 'Create Chatroom'} 114 </h2> 115 116 <div className="space-y-4"> 117 <div> 118 <label className="block text-[12px] font-600 text-text-2 mb-1.5">Name</label> 119 <input 120 type="text" 121 value={name} 122 onChange={(e) => setName(e.target.value)} 123 placeholder="e.g. Research Team" 124 className="w-full px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40" 125 /> 126 </div> 127 128 <div> 129 <label className="block text-[12px] font-600 text-text-2 mb-1.5">Description</label> 130 <input 131 type="text" 132 value={description} 133 onChange={(e) => setDescription(e.target.value)} 134 placeholder="Optional description" 135 className="w-full px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40" 136 /> 137 </div> 138 139 <div> 140 <label className="block text-[12px] font-600 text-text-2 mb-1.5">Response Mode</label> 141 <div className="flex rounded-[8px] border border-white/[0.08] overflow-hidden"> 142 {(['sequential', 'parallel'] as const).map((mode) => ( 143 <button 144 key={mode} 145 type="button" 146 onClick={() => setChatMode(mode)} 147 className={`flex-1 py-2 text-[12px] font-600 capitalize cursor-pointer transition-all ${ 148 chatMode === mode 149 ? 'bg-accent-soft text-accent-bright' 150 : 'bg-transparent text-text-3 hover:text-text-2' 151 }`} 152 > 153 {mode} 154 </button> 155 ))} 156 </div> 157 <p className="text-[11px] text-text-3 mt-1"> 158 {chatMode === 'parallel' 159 ? 'All mentioned agents respond simultaneously' 160 : 'Agents respond one at a time in order'} 161 </p> 162 </div> 163 164 <div> 165 <button 166 type="button" 167 onClick={() => setAutoAddress((value) => !value)} 168 className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-[8px] border border-white/[0.08] bg-white/[0.03] cursor-pointer transition-all hover:bg-white/[0.05]" 169 > 170 <div className={`w-8 h-[18px] rounded-full transition-all relative ${autoAddress ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}> 171 <div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all ${autoAddress ? 'left-[16px]' : 'left-[2px]'}`} /> 172 </div> 173 <div className="flex-1 text-left"> 174 <span className="text-[12px] font-600 text-text-2">Auto-address all agents</span> 175 <p className="text-[11px] text-text-3 mt-0.5"> 176 {autoAddress 177 ? 'Every message is sent to all agents, no @mention needed' 178 : 'Only agents you @mention respond unless routing guidance selects someone'} 179 </p> 180 </div> 181 </button> 182 </div> 183 184 <div> 185 <label className="block text-[12px] font-600 text-text-2 mb-1.5"> 186 Members ({selectedAgentIds.length} selected) 187 </label> 188 <p className="mb-2 text-[11px] text-text-3"> 189 Choose the agents who should be available in this room. Every chatroom needs at least one member. 190 </p> 191 <div className="max-h-[240px] overflow-y-auto rounded-[8px] border border-white/[0.08] bg-white/[0.03]"> 192 {agentList.length === 0 ? ( 193 <p className="p-3 text-[12px] text-text-3">No agents available</p> 194 ) : ( 195 agentList.map((agent) => { 196 const selected = selectedAgentIds.includes(agent.id) 197 return ( 198 <button 199 key={agent.id} 200 onClick={() => toggleAgent(agent.id)} 201 className={`w-full flex items-center gap-2.5 px-3 py-2 text-left transition-all cursor-pointer ${ 202 selected ? 'bg-accent-soft/40' : 'hover:bg-white/[0.04]' 203 }`} 204 > 205 <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} /> 206 <span className="text-[13px] text-text flex-1 truncate">{agent.name}</span> 207 {selected && ( 208 <CheckIcon size={14} className="text-accent-bright shrink-0" /> 209 )} 210 </button> 211 ) 212 }) 213 )} 214 </div> 215 {selectedAgentIds.length === 0 && ( 216 <p className="mt-2 text-[11px] text-amber-300"> 217 Select at least one member before creating the room. 218 </p> 219 )} 220 </div> 221 222 <div> 223 <label className="block text-[12px] font-600 text-text-2 mb-1.5">Routing Guidance</label> 224 <p className="text-[11px] text-text-3 mb-2"> 225 Optional. Used only when there is no explicit @mention and auto-address is off. Describe which kinds of messages should go to which members. 226 </p> 227 <textarea 228 value={routingGuidance} 229 onChange={(e) => setRoutingGuidance(e.target.value)} 230 placeholder={'Examples:\nRoute deployment issues to Ops.\nPrefer Maya for design reviews and UI polish.\nSend pricing or market-analysis requests to Research.'} 231 rows={6} 232 className="w-full px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40 resize-y min-h-[132px]" 233 /> 234 </div> 235 </div> 236 237 <div className="flex items-center gap-3 mt-6"> 238 <button 239 onClick={handleSave} 240 disabled={!name.trim() || saving || selectedAgentIds.length === 0} 241 className="flex-1 py-2.5 rounded-[8px] text-[13px] font-600 bg-accent-bright text-white hover:bg-accent-bright/90 transition-all disabled:opacity-50 cursor-pointer" 242 > 243 {saving ? 'Saving...' : editing ? 'Save Changes' : 'Create Chatroom'} 244 </button> 245 {editing && ( 246 <button 247 onClick={() => setConfirmDelete(true)} 248 disabled={saving} 249 className="py-2.5 px-4 rounded-[8px] text-[13px] font-600 text-red-400 hover:bg-red-500/10 transition-all cursor-pointer" 250 > 251 Delete 252 </button> 253 )} 254 </div> 255 </div> 256 <ConfirmDialog 257 open={confirmDelete} 258 title="Delete Chatroom?" 259 message={editing ? `Delete "${editing.name}"? This removes the chatroom and its configuration.` : 'Delete this chatroom?'} 260 confirmLabel={saving ? 'Deleting...' : 'Delete'} 261 confirmDisabled={saving} 262 cancelDisabled={saving} 263 danger 264 onConfirm={() => { void handleDelete() }} 265 onCancel={() => { if (!saving) setConfirmDelete(false) }} 266 /> 267 </BottomSheet> 268 ) 269 }