/ src / components / chatrooms / chatroom-sheet.tsx
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  }