@swarmclawai/swarmclaw 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
6
|
+
import rehypeHighlight from 'rehype-highlight'
|
|
7
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
|
+
import { CodeBlock } from '@/components/chat/code-block'
|
|
9
|
+
import { ReactionPicker } from './reaction-picker'
|
|
10
|
+
import { ReplyQuote } from '@/components/shared/reply-quote'
|
|
11
|
+
import { AttachmentChip, parseAttachmentUrl } from '@/components/shared/attachment-chip'
|
|
12
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
13
|
+
import { AgentHoverCard } from './agent-hover-card'
|
|
14
|
+
import { ChatroomToolRequestBanner } from './chatroom-tool-request-banner'
|
|
15
|
+
import { isStructuredMarkdown } from '@/components/chat/markdown-utils'
|
|
16
|
+
import { TransferAgentPicker } from '@/components/chat/transfer-agent-picker'
|
|
17
|
+
import type { ChatroomMessage, Agent } from '@/types'
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
message: ChatroomMessage
|
|
21
|
+
agents: Record<string, Agent>
|
|
22
|
+
onToggleReaction: (messageId: string, emoji: string) => void
|
|
23
|
+
onReply?: (message: ChatroomMessage) => void
|
|
24
|
+
onTogglePin?: (messageId: string) => void
|
|
25
|
+
onTransfer?: (messageId: string, targetAgentId: string) => void
|
|
26
|
+
pinnedMessageIds?: string[]
|
|
27
|
+
/** Set of agentIds currently streaming */
|
|
28
|
+
streamingAgentIds?: Set<string>
|
|
29
|
+
/** All messages in the chatroom, for resolving replyToId */
|
|
30
|
+
messages?: ChatroomMessage[]
|
|
31
|
+
/** Whether this message is grouped with the previous (same sender within 2min) */
|
|
32
|
+
grouped?: boolean
|
|
33
|
+
/** Moment overlay to display above the avatar (heartbeat/tool activity) */
|
|
34
|
+
momentOverlay?: React.ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatRelativeTime(ts: number): string {
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
const diffSec = Math.floor((now - ts) / 1000)
|
|
40
|
+
if (diffSec < 60) return 'just now'
|
|
41
|
+
const diffMin = Math.floor(diffSec / 60)
|
|
42
|
+
if (diffMin < 60) return `${diffMin}m ago`
|
|
43
|
+
const diffHr = Math.floor(diffMin / 60)
|
|
44
|
+
if (diffHr < 24) return `${diffHr}h ago`
|
|
45
|
+
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function navigateToAgent(agentId: string) {
|
|
49
|
+
useAppStore.getState().setActiveView('agents')
|
|
50
|
+
useAppStore.getState().setCurrentAgent(agentId)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Pre-process @mentions into markdown-friendly format for ReactMarkdown */
|
|
54
|
+
function preprocessMentions(text: string, agents: Record<string, Agent>): string {
|
|
55
|
+
const nameToId = new Map<string, string>()
|
|
56
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
57
|
+
nameToId.set(agent.name.toLowerCase().replace(/\s+/g, ''), id)
|
|
58
|
+
}
|
|
59
|
+
return text.replace(/@(\S+)/g, (match, name) => {
|
|
60
|
+
const agentId = nameToId.get(name.toLowerCase())
|
|
61
|
+
if (agentId) {
|
|
62
|
+
return `[@${name}](#agent:${agentId})`
|
|
63
|
+
}
|
|
64
|
+
// Unrecognized mentions still get styled as mention links
|
|
65
|
+
return `[@${name}](#mention:${name})`
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Group reactions by emoji */
|
|
70
|
+
function groupReactions(reactions: Array<{ emoji: string; reactorId: string }>): Array<{ emoji: string; count: number; hasUser: boolean }> {
|
|
71
|
+
const map = new Map<string, { count: number; hasUser: boolean }>()
|
|
72
|
+
for (const r of reactions) {
|
|
73
|
+
const existing = map.get(r.emoji) || { count: 0, hasUser: false }
|
|
74
|
+
existing.count++
|
|
75
|
+
if (r.reactorId === 'user') existing.hasUser = true
|
|
76
|
+
map.set(r.emoji, existing)
|
|
77
|
+
}
|
|
78
|
+
return Array.from(map.entries()).map(([emoji, data]) => ({ emoji, ...data }))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// TransferAgentPicker imported from @/components/chat/transfer-agent-picker
|
|
82
|
+
|
|
83
|
+
/** Render chatroom message attachments */
|
|
84
|
+
function renderChatroomAttachments(message: ChatroomMessage) {
|
|
85
|
+
const isUser = message.senderId === 'user'
|
|
86
|
+
const seen = new Set<string>()
|
|
87
|
+
const chips: { url: string; filename: string }[] = []
|
|
88
|
+
|
|
89
|
+
if (message.imagePath) {
|
|
90
|
+
const primary = parseAttachmentUrl(message.imagePath)
|
|
91
|
+
if (primary.url) {
|
|
92
|
+
seen.add(primary.url)
|
|
93
|
+
chips.push(primary)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (message.attachedFiles?.length) {
|
|
97
|
+
for (const fp of message.attachedFiles) {
|
|
98
|
+
const att = parseAttachmentUrl(fp)
|
|
99
|
+
if (att.url && !seen.has(att.url)) {
|
|
100
|
+
seen.add(att.url)
|
|
101
|
+
chips.push(att)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!chips.length) return null
|
|
106
|
+
return (
|
|
107
|
+
<div className="flex flex-col">
|
|
108
|
+
{chips.map((c) => <AttachmentChip key={c.url} url={c.url} filename={c.filename} isUserMsg={isUser} />)}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function ChatroomMessageBubble({ message, agents, onToggleReaction, onReply, onTogglePin, onTransfer, pinnedMessageIds, streamingAgentIds, messages, grouped: isGrouped, momentOverlay }: Props) {
|
|
114
|
+
const [showPicker, setShowPicker] = useState(false)
|
|
115
|
+
const [showTransferPicker, setShowTransferPicker] = useState(false)
|
|
116
|
+
const userAvatarSeed = useAppStore((s) => s.appSettings.userAvatarSeed)
|
|
117
|
+
const wide = isStructuredMarkdown(message.text)
|
|
118
|
+
|
|
119
|
+
// System event messages (join/leave)
|
|
120
|
+
if (message.senderId === 'system') {
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex justify-center py-1.5 px-4">
|
|
123
|
+
<span className="text-[11px] text-text-3/50 font-500 flex items-center gap-1.5">
|
|
124
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/40">
|
|
125
|
+
{message.text.includes('left') ? (
|
|
126
|
+
<><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" y1="12" x2="9" y2="12" /></>
|
|
127
|
+
) : (
|
|
128
|
+
<><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" /><polyline points="10 17 15 12 10 7" /><line x1="15" y1="12" x2="3" y2="12" /></>
|
|
129
|
+
)}
|
|
130
|
+
</svg>
|
|
131
|
+
{message.text}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isUser = message.senderId === 'user'
|
|
138
|
+
const agent = !isUser ? agents[message.senderId] : null
|
|
139
|
+
const groupedReactions = groupReactions(message.reactions)
|
|
140
|
+
|
|
141
|
+
// Resolve reply-to message
|
|
142
|
+
const replyToMessage = message.replyToId && messages
|
|
143
|
+
? messages.find((m) => m.id === message.replyToId)
|
|
144
|
+
: null
|
|
145
|
+
|
|
146
|
+
// Pre-process text for markdown rendering
|
|
147
|
+
const processedText = preprocessMentions(message.text, agents)
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
id={`chatroom-msg-${message.id}`}
|
|
152
|
+
className={`group flex gap-2.5 px-4 hover:bg-white/[0.02] ${isGrouped ? 'py-0.5' : 'py-1.5'}`}
|
|
153
|
+
style={{ animation: 'msg-in 0.25s ease-out both' }}
|
|
154
|
+
>
|
|
155
|
+
{/* Avatar or spacer */}
|
|
156
|
+
<div className="shrink-0 mt-0.5 w-7 relative">
|
|
157
|
+
{!isGrouped && (
|
|
158
|
+
isUser ? (
|
|
159
|
+
userAvatarSeed ? (
|
|
160
|
+
<div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
|
|
161
|
+
<AgentAvatar seed={userAvatarSeed} name={message.senderName} size={28} />
|
|
162
|
+
</div>
|
|
163
|
+
) : (
|
|
164
|
+
<div className="w-7 h-7 rounded-full bg-white/[0.08] flex items-center justify-center text-[11px] font-600 text-text-2">
|
|
165
|
+
You
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
) : agent ? (
|
|
169
|
+
<button
|
|
170
|
+
onClick={() => navigateToAgent(message.senderId)}
|
|
171
|
+
className="bg-transparent border-none p-0 cursor-pointer transition-all duration-150 hover:scale-110 hover:-translate-y-0.5"
|
|
172
|
+
style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}
|
|
173
|
+
>
|
|
174
|
+
<AgentAvatar seed={agent.avatarSeed || null} name={message.senderName} size={28} status={streamingAgentIds?.has(message.senderId) ? 'busy' : 'online'} />
|
|
175
|
+
</button>
|
|
176
|
+
) : (
|
|
177
|
+
<div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
|
|
178
|
+
<AgentAvatar seed={null} name={message.senderName} size={28} />
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
)}
|
|
182
|
+
{momentOverlay}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Content */}
|
|
186
|
+
<div className="flex-1 min-w-0">
|
|
187
|
+
{!isGrouped && (
|
|
188
|
+
<div className="flex items-baseline gap-2 mb-0.5">
|
|
189
|
+
{!isUser && agent ? (
|
|
190
|
+
<AgentHoverCard agent={agent}>
|
|
191
|
+
<span className="text-[13px] font-600 text-accent-bright hover:underline cursor-pointer">
|
|
192
|
+
{message.senderName}
|
|
193
|
+
</span>
|
|
194
|
+
</AgentHoverCard>
|
|
195
|
+
) : (
|
|
196
|
+
<span className="text-[13px] font-600 text-text">
|
|
197
|
+
{message.senderName}
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
<span className="label-mono" title={new Date(message.time).toLocaleString()}>{formatRelativeTime(message.time)}</span>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{/* Reply quote */}
|
|
205
|
+
{replyToMessage && (
|
|
206
|
+
<ReplyQuote
|
|
207
|
+
senderName={replyToMessage.senderName}
|
|
208
|
+
text={replyToMessage.text}
|
|
209
|
+
onClick={() => {
|
|
210
|
+
const el = document.getElementById(`chatroom-msg-${replyToMessage.id}`)
|
|
211
|
+
if (el) {
|
|
212
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
213
|
+
el.classList.add('bg-accent-soft/20')
|
|
214
|
+
setTimeout(() => el.classList.remove('bg-accent-soft/20'), 2000)
|
|
215
|
+
}
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Attachments */}
|
|
221
|
+
{renderChatroomAttachments(message)}
|
|
222
|
+
|
|
223
|
+
{/* Message text with markdown */}
|
|
224
|
+
<div className={`text-[13px] text-text leading-[1.5] break-words chatroom-prose ${wide ? 'max-w-[92%]' : 'max-w-[85%]'}`}>
|
|
225
|
+
<ReactMarkdown
|
|
226
|
+
remarkPlugins={[remarkGfm]}
|
|
227
|
+
rehypePlugins={[rehypeHighlight]}
|
|
228
|
+
components={{
|
|
229
|
+
pre({ children }) {
|
|
230
|
+
return <pre>{children}</pre>
|
|
231
|
+
},
|
|
232
|
+
code({ className, children }) {
|
|
233
|
+
const isBlock = className?.startsWith('language-') || className?.startsWith('hljs')
|
|
234
|
+
if (isBlock) {
|
|
235
|
+
return <CodeBlock className={className}>{children}</CodeBlock>
|
|
236
|
+
}
|
|
237
|
+
return (
|
|
238
|
+
<code className="px-1 py-0.5 rounded bg-white/[0.08] text-[12px] font-mono text-accent-bright/90">
|
|
239
|
+
{children}
|
|
240
|
+
</code>
|
|
241
|
+
)
|
|
242
|
+
},
|
|
243
|
+
a({ href, children }) {
|
|
244
|
+
if (!href) return <>{children}</>
|
|
245
|
+
// Agent mention links (recognized agents — hover card)
|
|
246
|
+
if (href.startsWith('#agent:')) {
|
|
247
|
+
const agentId = href.replace('#agent:', '')
|
|
248
|
+
const mentionAgent = agents[agentId]
|
|
249
|
+
if (mentionAgent) {
|
|
250
|
+
return (
|
|
251
|
+
<AgentHoverCard agent={mentionAgent}>
|
|
252
|
+
<span className="text-accent-bright font-600 bg-accent-soft/40 px-0.5 rounded hover:underline cursor-pointer">
|
|
253
|
+
{children}
|
|
254
|
+
</span>
|
|
255
|
+
</AgentHoverCard>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
return (
|
|
259
|
+
<span className="text-accent-bright font-600 bg-accent-soft/40 px-0.5 rounded">
|
|
260
|
+
{children}
|
|
261
|
+
</span>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
// Unrecognized @mention — styled but not clickable
|
|
265
|
+
if (href.startsWith('#mention:')) {
|
|
266
|
+
return (
|
|
267
|
+
<span className="text-accent-bright font-600 bg-accent-soft/40 px-0.5 rounded">
|
|
268
|
+
{children}
|
|
269
|
+
</span>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
// YouTube embeds
|
|
273
|
+
const ytMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/)
|
|
274
|
+
if (ytMatch) {
|
|
275
|
+
return (
|
|
276
|
+
<div className="my-2">
|
|
277
|
+
<iframe
|
|
278
|
+
src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
|
279
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
280
|
+
allowFullScreen
|
|
281
|
+
className="w-full max-w-[480px] aspect-video rounded-[8px] border border-white/[0.06]"
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
// Upload links
|
|
287
|
+
if (typeof href === 'string' && href.includes('/api/uploads/')) {
|
|
288
|
+
const filename = href.split('/').pop() || 'file'
|
|
289
|
+
return (
|
|
290
|
+
<a href={href} download={filename} className="text-accent-bright hover:underline">
|
|
291
|
+
{children}
|
|
292
|
+
</a>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
// Default external link
|
|
296
|
+
return (
|
|
297
|
+
<a href={href} target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">
|
|
298
|
+
{children}
|
|
299
|
+
</a>
|
|
300
|
+
)
|
|
301
|
+
},
|
|
302
|
+
img({ src, alt }) {
|
|
303
|
+
if (!src || typeof src !== 'string') return null
|
|
304
|
+
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(src)
|
|
305
|
+
if (isVideo) {
|
|
306
|
+
return <video src={src} controls preload="none" className="max-w-full rounded-[8px] my-2" />
|
|
307
|
+
}
|
|
308
|
+
return (
|
|
309
|
+
<a href={src} download target="_blank" rel="noopener noreferrer" className="block my-2">
|
|
310
|
+
<img src={src} alt={alt || 'Image'} loading="lazy" className="max-w-full max-h-[400px] rounded-[8px] border border-white/[0.06]" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
311
|
+
</a>
|
|
312
|
+
)
|
|
313
|
+
},
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
{processedText}
|
|
317
|
+
</ReactMarkdown>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Tool request banner for agent messages */}
|
|
321
|
+
{!isUser && agent && (
|
|
322
|
+
<ChatroomToolRequestBanner
|
|
323
|
+
agentId={message.senderId}
|
|
324
|
+
agentName={message.senderName}
|
|
325
|
+
text={message.text}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* Reactions */}
|
|
330
|
+
{groupedReactions.length > 0 && (
|
|
331
|
+
<div className="flex flex-wrap gap-1 mt-1.5">
|
|
332
|
+
{groupedReactions.map(({ emoji, count, hasUser }) => (
|
|
333
|
+
<button
|
|
334
|
+
key={emoji}
|
|
335
|
+
onClick={() => onToggleReaction(message.id, emoji)}
|
|
336
|
+
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] transition-all cursor-pointer ${
|
|
337
|
+
hasUser
|
|
338
|
+
? 'bg-[#1a1a3a] border border-accent-bright/30'
|
|
339
|
+
: 'bg-[#16162a] border border-white/[0.1] hover:bg-[#1e1e38]'
|
|
340
|
+
}`}
|
|
341
|
+
>
|
|
342
|
+
<span>{emoji}</span>
|
|
343
|
+
{count > 1 && <span className="text-text-3">{count}</span>}
|
|
344
|
+
</button>
|
|
345
|
+
))}
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Action buttons (reply + pin + transfer + reaction) */}
|
|
351
|
+
<div className="relative shrink-0 mt-0.5 flex items-start gap-0.5" style={{ zIndex: showPicker || showTransferPicker ? 50 : undefined }}>
|
|
352
|
+
{/* Reply button */}
|
|
353
|
+
{onReply && (
|
|
354
|
+
<button
|
|
355
|
+
onClick={() => onReply(message)}
|
|
356
|
+
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
357
|
+
title="Reply"
|
|
358
|
+
>
|
|
359
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
360
|
+
<polyline points="9 17 4 12 9 7" />
|
|
361
|
+
<path d="M20 18v-2a4 4 0 0 0-4-4H4" />
|
|
362
|
+
</svg>
|
|
363
|
+
</button>
|
|
364
|
+
)}
|
|
365
|
+
{/* Pin button */}
|
|
366
|
+
{onTogglePin && (
|
|
367
|
+
<button
|
|
368
|
+
onClick={() => onTogglePin(message.id)}
|
|
369
|
+
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
370
|
+
title={pinnedMessageIds?.includes(message.id) ? 'Unpin message' : 'Pin message'}
|
|
371
|
+
>
|
|
372
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill={pinnedMessageIds?.includes(message.id) ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={pinnedMessageIds?.includes(message.id) ? 'text-amber-400' : 'text-text-3'}>
|
|
373
|
+
<path d="M12 17v5" />
|
|
374
|
+
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16h14v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 2-2H6a2 2 0 0 0 2 2 1 1 0 0 1 1 1z" />
|
|
375
|
+
</svg>
|
|
376
|
+
</button>
|
|
377
|
+
)}
|
|
378
|
+
{/* Transfer button */}
|
|
379
|
+
{onTransfer && !isUser && (
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => setShowTransferPicker(!showTransferPicker)}
|
|
382
|
+
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
383
|
+
title="Transfer to agent"
|
|
384
|
+
>
|
|
385
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
386
|
+
<path d="M8 3L4 7l4 4" />
|
|
387
|
+
<path d="M4 7h16" />
|
|
388
|
+
<path d="M16 21l4-4-4-4" />
|
|
389
|
+
<path d="M20 17H4" />
|
|
390
|
+
</svg>
|
|
391
|
+
</button>
|
|
392
|
+
)}
|
|
393
|
+
{showTransferPicker && onTransfer && (
|
|
394
|
+
<TransferAgentPicker
|
|
395
|
+
excludeIds={[message.senderId]}
|
|
396
|
+
onSelect={(targetId) => {
|
|
397
|
+
onTransfer(message.id, targetId)
|
|
398
|
+
setShowTransferPicker(false)
|
|
399
|
+
}}
|
|
400
|
+
onClose={() => setShowTransferPicker(false)}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
{/* Reaction button */}
|
|
404
|
+
<button
|
|
405
|
+
onClick={() => setShowPicker(!showPicker)}
|
|
406
|
+
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
407
|
+
>
|
|
408
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
409
|
+
<circle cx="12" cy="12" r="10" />
|
|
410
|
+
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
|
411
|
+
<line x1="9" y1="9" x2="9.01" y2="9" />
|
|
412
|
+
<line x1="15" y1="9" x2="15.01" y2="9" />
|
|
413
|
+
</svg>
|
|
414
|
+
</button>
|
|
415
|
+
{showPicker && (
|
|
416
|
+
<ReactionPicker
|
|
417
|
+
onSelect={(emoji) => {
|
|
418
|
+
onToggleReaction(message.id, emoji)
|
|
419
|
+
setShowPicker(false)
|
|
420
|
+
}}
|
|
421
|
+
onClose={() => setShowPicker(false)}
|
|
422
|
+
/>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
)
|
|
427
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
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 { toast } from 'sonner'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
+
import type { Agent } from '@/types'
|
|
10
|
+
import { CheckIcon } from '@/components/shared/check-icon'
|
|
11
|
+
|
|
12
|
+
export function ChatroomSheet() {
|
|
13
|
+
const open = useChatroomStore((s) => s.chatroomSheetOpen)
|
|
14
|
+
const editingId = useChatroomStore((s) => s.editingChatroomId)
|
|
15
|
+
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
16
|
+
const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen)
|
|
17
|
+
const createChatroom = useChatroomStore((s) => s.createChatroom)
|
|
18
|
+
const updateChatroom = useChatroomStore((s) => s.updateChatroom)
|
|
19
|
+
const deleteChatroom = useChatroomStore((s) => s.deleteChatroom)
|
|
20
|
+
const setCurrentChatroom = useChatroomStore((s) => s.setCurrentChatroom)
|
|
21
|
+
const agents = useAppStore((s) => s.agents)
|
|
22
|
+
|
|
23
|
+
const [name, setName] = useState('')
|
|
24
|
+
const [description, setDescription] = useState('')
|
|
25
|
+
const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([])
|
|
26
|
+
const [chatMode, setChatMode] = useState<'sequential' | 'parallel'>('sequential')
|
|
27
|
+
const [autoAddress, setAutoAddress] = useState(false)
|
|
28
|
+
const [saving, setSaving] = useState(false)
|
|
29
|
+
|
|
30
|
+
const editing = editingId ? chatrooms[editingId] : null
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (editing) {
|
|
34
|
+
setName(editing.name)
|
|
35
|
+
setDescription(editing.description || '')
|
|
36
|
+
setSelectedAgentIds([...editing.agentIds])
|
|
37
|
+
setChatMode(editing.chatMode || 'sequential')
|
|
38
|
+
setAutoAddress(editing.autoAddress || false)
|
|
39
|
+
} else {
|
|
40
|
+
setName('')
|
|
41
|
+
setDescription('')
|
|
42
|
+
setSelectedAgentIds([])
|
|
43
|
+
setChatMode('sequential')
|
|
44
|
+
setAutoAddress(false)
|
|
45
|
+
}
|
|
46
|
+
}, [editing, open])
|
|
47
|
+
|
|
48
|
+
const handleSave = async () => {
|
|
49
|
+
if (!name.trim() || saving) return
|
|
50
|
+
setSaving(true)
|
|
51
|
+
try {
|
|
52
|
+
if (editing) {
|
|
53
|
+
await updateChatroom(editing.id, { name, description, agentIds: selectedAgentIds, chatMode, autoAddress })
|
|
54
|
+
toast.success('Chatroom saved')
|
|
55
|
+
} else {
|
|
56
|
+
const chatroom = await createChatroom({ name, description, agentIds: selectedAgentIds, chatMode, autoAddress })
|
|
57
|
+
setCurrentChatroom(chatroom.id)
|
|
58
|
+
toast.success('Chatroom created')
|
|
59
|
+
}
|
|
60
|
+
setChatroomSheetOpen(false)
|
|
61
|
+
} finally {
|
|
62
|
+
setSaving(false)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleDelete = async () => {
|
|
67
|
+
if (!editing || saving) return
|
|
68
|
+
setSaving(true)
|
|
69
|
+
try {
|
|
70
|
+
await deleteChatroom(editing.id)
|
|
71
|
+
toast.success('Chatroom deleted')
|
|
72
|
+
setChatroomSheetOpen(false)
|
|
73
|
+
} finally {
|
|
74
|
+
setSaving(false)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const toggleAgent = (agentId: string) => {
|
|
79
|
+
setSelectedAgentIds((prev) =>
|
|
80
|
+
prev.includes(agentId) ? prev.filter((id) => id !== agentId) : [...prev, agentId]
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const agentList = Object.values(agents).filter(
|
|
85
|
+
(a: Agent) => !a.trashedAt
|
|
86
|
+
) as Agent[]
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<BottomSheet open={open} onClose={() => setChatroomSheetOpen(false)}>
|
|
90
|
+
<div className="p-6 max-w-[560px] mx-auto">
|
|
91
|
+
<h2 className="font-display text-[18px] font-700 text-text mb-4">
|
|
92
|
+
{editing ? 'Edit Chatroom' : 'Create Chatroom'}
|
|
93
|
+
</h2>
|
|
94
|
+
|
|
95
|
+
<div className="space-y-4">
|
|
96
|
+
<div>
|
|
97
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">Name</label>
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
value={name}
|
|
101
|
+
onChange={(e) => setName(e.target.value)}
|
|
102
|
+
placeholder="e.g. Research Team"
|
|
103
|
+
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"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div>
|
|
108
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">Description</label>
|
|
109
|
+
<input
|
|
110
|
+
type="text"
|
|
111
|
+
value={description}
|
|
112
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
113
|
+
placeholder="Optional description"
|
|
114
|
+
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"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">Response Mode</label>
|
|
120
|
+
<div className="flex rounded-[8px] border border-white/[0.08] overflow-hidden">
|
|
121
|
+
{(['sequential', 'parallel'] as const).map((mode) => (
|
|
122
|
+
<button
|
|
123
|
+
key={mode}
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => setChatMode(mode)}
|
|
126
|
+
className={`flex-1 py-2 text-[12px] font-600 capitalize cursor-pointer transition-all ${
|
|
127
|
+
chatMode === mode
|
|
128
|
+
? 'bg-accent-soft text-accent-bright'
|
|
129
|
+
: 'bg-transparent text-text-3 hover:text-text-2'
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
{mode}
|
|
133
|
+
</button>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
<p className="text-[11px] text-text-3 mt-1">
|
|
137
|
+
{chatMode === 'parallel'
|
|
138
|
+
? 'All mentioned agents respond simultaneously'
|
|
139
|
+
: 'Agents respond one at a time in order'}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => setAutoAddress((v) => !v)}
|
|
147
|
+
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]"
|
|
148
|
+
>
|
|
149
|
+
<div className={`w-8 h-[18px] rounded-full transition-all relative ${autoAddress ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}>
|
|
150
|
+
<div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all ${autoAddress ? 'left-[16px]' : 'left-[2px]'}`} />
|
|
151
|
+
</div>
|
|
152
|
+
<div className="flex-1 text-left">
|
|
153
|
+
<span className="text-[12px] font-600 text-text-2">Auto-address all agents</span>
|
|
154
|
+
<p className="text-[11px] text-text-3 mt-0.5">
|
|
155
|
+
{autoAddress
|
|
156
|
+
? 'Every message is sent to all agents, no @mention needed'
|
|
157
|
+
: 'Only agents you @mention will respond'}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div>
|
|
164
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">
|
|
165
|
+
Members ({selectedAgentIds.length} selected)
|
|
166
|
+
</label>
|
|
167
|
+
<div className="max-h-[240px] overflow-y-auto rounded-[8px] border border-white/[0.08] bg-white/[0.03]">
|
|
168
|
+
{agentList.length === 0 ? (
|
|
169
|
+
<p className="p-3 text-[12px] text-text-3">No agents available</p>
|
|
170
|
+
) : (
|
|
171
|
+
agentList.map((agent) => {
|
|
172
|
+
const selected = selectedAgentIds.includes(agent.id)
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
key={agent.id}
|
|
176
|
+
onClick={() => toggleAgent(agent.id)}
|
|
177
|
+
className={`w-full flex items-center gap-2.5 px-3 py-2 text-left transition-all cursor-pointer ${
|
|
178
|
+
selected ? 'bg-accent-soft/40' : 'hover:bg-white/[0.04]'
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
|
|
182
|
+
<span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
|
|
183
|
+
{selected && (
|
|
184
|
+
<CheckIcon size={14} className="text-accent-bright shrink-0" />
|
|
185
|
+
)}
|
|
186
|
+
</button>
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="flex items-center gap-3 mt-6">
|
|
195
|
+
<button
|
|
196
|
+
onClick={handleSave}
|
|
197
|
+
disabled={!name.trim() || saving}
|
|
198
|
+
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"
|
|
199
|
+
>
|
|
200
|
+
{saving ? 'Saving...' : editing ? 'Save Changes' : 'Create Chatroom'}
|
|
201
|
+
</button>
|
|
202
|
+
{editing && (
|
|
203
|
+
<button
|
|
204
|
+
onClick={handleDelete}
|
|
205
|
+
disabled={saving}
|
|
206
|
+
className="py-2.5 px-4 rounded-[8px] text-[13px] font-600 text-red-400 hover:bg-red-500/10 transition-all cursor-pointer"
|
|
207
|
+
>
|
|
208
|
+
Delete
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</BottomSheet>
|
|
214
|
+
)
|
|
215
|
+
}
|