@swarmclawai/swarmclaw 0.6.4 → 0.6.7
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 +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- package/tsconfig.json +2 -1
|
@@ -10,6 +10,7 @@ import { useChatStore } from '@/stores/use-chat-store'
|
|
|
10
10
|
interface Props {
|
|
11
11
|
assistantName?: string
|
|
12
12
|
agentAvatarSeed?: string
|
|
13
|
+
agentAvatarUrl?: string | null
|
|
13
14
|
agentName?: string
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -34,7 +35,7 @@ function ElapsedTimer({ startTime }: { startTime: number }) {
|
|
|
34
35
|
)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }: Props) {
|
|
38
|
+
export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarUrl, agentName }: Props) {
|
|
38
39
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
39
40
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
40
41
|
const thinkingText = useChatStore((s) => s.thinkingText)
|
|
@@ -50,7 +51,7 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }:
|
|
|
50
51
|
<div className="flex flex-col items-start relative pl-[44px]"
|
|
51
52
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
52
53
|
<div className="absolute left-[4px] top-0">
|
|
53
|
-
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
54
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
54
55
|
</div>
|
|
55
56
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
56
57
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
@@ -52,7 +52,7 @@ export function TransferAgentPicker({ excludeIds, filterIds, onSelect, onClose }
|
|
|
52
52
|
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none"
|
|
53
53
|
style={{ fontFamily: 'inherit' }}
|
|
54
54
|
>
|
|
55
|
-
<AgentAvatar seed={a.avatarSeed} name={a.name} size={20} />
|
|
55
|
+
<AgentAvatar seed={a.avatarSeed} avatarUrl={a.avatarUrl} name={a.name} size={20} />
|
|
56
56
|
<span className="text-[12px] text-text truncate">{a.name}</span>
|
|
57
57
|
</button>
|
|
58
58
|
))}
|
|
@@ -46,7 +46,7 @@ export function AgentHoverCard({ agent, children, status }: Props) {
|
|
|
46
46
|
<HoverCardContent align="start" className="w-[280px]">
|
|
47
47
|
{/* Header: avatar + name + model */}
|
|
48
48
|
<div className="flex items-center gap-2">
|
|
49
|
-
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={28} status={status} />
|
|
49
|
+
<AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={28} status={status} />
|
|
50
50
|
<div className="min-w-0 flex-1">
|
|
51
51
|
<div className="text-[13px] font-600 text-text truncate">{agent.name}</div>
|
|
52
52
|
<div className="label-mono truncate">{agent.model}</div>
|
|
@@ -203,7 +203,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
203
203
|
selectedIndex === i + 1 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
|
|
204
204
|
}`}
|
|
205
205
|
>
|
|
206
|
-
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={20} />
|
|
206
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={20} />
|
|
207
207
|
<span className="text-[13px] text-text">{agent.name}</span>
|
|
208
208
|
</button>
|
|
209
209
|
))}
|
|
@@ -15,7 +15,7 @@ import { ChatroomToolRequestBanner } from './chatroom-tool-request-banner'
|
|
|
15
15
|
import { isStructuredMarkdown } from '@/components/chat/markdown-utils'
|
|
16
16
|
import { TransferAgentPicker } from '@/components/chat/transfer-agent-picker'
|
|
17
17
|
import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
|
|
18
|
-
import type { ChatroomMessage, Agent } from '@/types'
|
|
18
|
+
import type { ChatroomMessage, Chatroom, Agent } from '@/types'
|
|
19
19
|
|
|
20
20
|
interface Props {
|
|
21
21
|
message: ChatroomMessage
|
|
@@ -24,6 +24,11 @@ interface Props {
|
|
|
24
24
|
onReply?: (message: ChatroomMessage) => void
|
|
25
25
|
onTogglePin?: (messageId: string) => void
|
|
26
26
|
onTransfer?: (messageId: string, targetAgentId: string) => void
|
|
27
|
+
onDeleteMessage?: (messageId: string, targetAgentId: string) => void
|
|
28
|
+
onMuteAgent?: (agentId: string) => void
|
|
29
|
+
onUnmuteAgent?: (agentId: string) => void
|
|
30
|
+
onSetRole?: (agentId: string, role: 'admin' | 'moderator' | 'member') => void
|
|
31
|
+
chatroom?: Chatroom
|
|
27
32
|
pinnedMessageIds?: string[]
|
|
28
33
|
/** Set of agentIds currently streaming */
|
|
29
34
|
streamingAgentIds?: Set<string>
|
|
@@ -51,6 +56,25 @@ function navigateToAgent(agentId: string) {
|
|
|
51
56
|
useAppStore.getState().setCurrentAgent(agentId)
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
function getMemberRoleFromChatroom(chatroom: Chatroom | undefined, agentId: string): string {
|
|
60
|
+
if (!chatroom?.members?.length) return 'member'
|
|
61
|
+
const member = chatroom.members.find((m) => m.agentId === agentId)
|
|
62
|
+
return member?.role || 'member'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isAgentMutedInChatroom(chatroom: Chatroom | undefined, agentId: string): boolean {
|
|
66
|
+
if (!chatroom?.members?.length) return false
|
|
67
|
+
const member = chatroom.members.find((m) => m.agentId === agentId)
|
|
68
|
+
if (!member?.mutedUntil) return false
|
|
69
|
+
return new Date(member.mutedUntil).getTime() > Date.now()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function roleBadgeStyle(role: string): { label: string; className: string } | null {
|
|
73
|
+
if (role === 'admin') return { label: 'Admin', className: 'bg-purple-500/20 text-purple-400 border-purple-500/30' }
|
|
74
|
+
if (role === 'moderator') return { label: 'Mod', className: 'bg-blue-500/20 text-blue-400 border-blue-500/30' }
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
/** Pre-process @mentions into markdown-friendly format for ReactMarkdown */
|
|
55
79
|
function preprocessMentions(text: string, agents: Record<string, Agent>): string {
|
|
56
80
|
const nameToId = new Map<string, string>()
|
|
@@ -111,9 +135,10 @@ function renderChatroomAttachments(message: ChatroomMessage) {
|
|
|
111
135
|
)
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
export function ChatroomMessageBubble({ message, agents, onToggleReaction, onReply, onTogglePin, onTransfer, pinnedMessageIds, streamingAgentIds, messages, grouped: isGrouped, momentOverlay }: Props) {
|
|
138
|
+
export function ChatroomMessageBubble({ message, agents, onToggleReaction, onReply, onTogglePin, onTransfer, onDeleteMessage, onMuteAgent, onUnmuteAgent, onSetRole, chatroom, pinnedMessageIds, streamingAgentIds, messages, grouped: isGrouped, momentOverlay }: Props) {
|
|
115
139
|
const [showPicker, setShowPicker] = useState(false)
|
|
116
140
|
const [showTransferPicker, setShowTransferPicker] = useState(false)
|
|
141
|
+
const [showModMenu, setShowModMenu] = useState(false)
|
|
117
142
|
const userAvatarSeed = useAppStore((s) => s.appSettings.userAvatarSeed)
|
|
118
143
|
const wide = isStructuredMarkdown(message.text)
|
|
119
144
|
|
|
@@ -172,7 +197,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
172
197
|
className="bg-transparent border-none p-0 cursor-pointer transition-all duration-150 hover:scale-110 hover:-translate-y-0.5"
|
|
173
198
|
style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}
|
|
174
199
|
>
|
|
175
|
-
<AgentAvatar seed={agent.avatarSeed || null} name={message.senderName} size={28} status={streamingAgentIds?.has(message.senderId) ? 'busy' : 'online'} />
|
|
200
|
+
<AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={message.senderName} size={28} status={streamingAgentIds?.has(message.senderId) ? 'busy' : 'online'} />
|
|
176
201
|
</button>
|
|
177
202
|
) : (
|
|
178
203
|
<div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
|
|
@@ -185,26 +210,41 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
185
210
|
|
|
186
211
|
{/* Content */}
|
|
187
212
|
<div className="flex-1 min-w-0">
|
|
188
|
-
{!isGrouped && (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
213
|
+
{!isGrouped && (() => {
|
|
214
|
+
const role = !isUser ? getMemberRoleFromChatroom(chatroom, message.senderId) : 'member'
|
|
215
|
+
const badge = !isUser ? roleBadgeStyle(role) : null
|
|
216
|
+
const muted = !isUser ? isAgentMutedInChatroom(chatroom, message.senderId) : false
|
|
217
|
+
return (
|
|
218
|
+
<div className="flex items-baseline gap-2 mb-0.5">
|
|
219
|
+
{!isUser && agent ? (
|
|
220
|
+
<AgentHoverCard agent={agent}>
|
|
221
|
+
<span className="text-[13px] font-600 text-accent-bright hover:underline cursor-pointer flex items-center gap-1.5">
|
|
222
|
+
{message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
|
|
223
|
+
{message.senderName}
|
|
224
|
+
</span>
|
|
225
|
+
</AgentHoverCard>
|
|
226
|
+
) : (
|
|
227
|
+
<span className="text-[13px] font-600 text-text flex items-center gap-1.5">
|
|
193
228
|
{message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
|
|
194
|
-
{message.senderName
|
|
229
|
+
{isUser && message.source?.senderName
|
|
230
|
+
? `${message.source.senderName} via ${CONNECTOR_PLATFORM_META[message.source.platform]?.label || message.source.platform}`
|
|
231
|
+
: message.senderName}
|
|
195
232
|
</span>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
)}
|
|
234
|
+
{badge && (
|
|
235
|
+
<span className={`text-[9px] font-600 px-1 py-0.5 rounded border leading-none ${badge.className}`}>
|
|
236
|
+
{badge.label}
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
{muted && (
|
|
240
|
+
<span className="text-[9px] font-600 px-1 py-0.5 rounded border leading-none bg-red-500/20 text-red-400 border-red-500/30">
|
|
241
|
+
Muted
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
<span className="label-mono" title={new Date(message.time).toLocaleString()}>{formatRelativeTime(message.time)}</span>
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
})()}
|
|
208
248
|
|
|
209
249
|
{/* Reply quote */}
|
|
210
250
|
{replyToMessage && (
|
|
@@ -352,8 +392,8 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
352
392
|
)}
|
|
353
393
|
</div>
|
|
354
394
|
|
|
355
|
-
{/* Action buttons (reply + pin + transfer + reaction) */}
|
|
356
|
-
<div className="relative shrink-0 mt-0.5 flex items-start gap-0.5" style={{ zIndex: showPicker || showTransferPicker ? 50 : undefined }}>
|
|
395
|
+
{/* Action buttons (reply + pin + transfer + moderate + reaction) */}
|
|
396
|
+
<div className="relative shrink-0 mt-0.5 flex items-start gap-0.5" style={{ zIndex: showPicker || showTransferPicker || showModMenu ? 50 : undefined }}>
|
|
357
397
|
{/* Reply button */}
|
|
358
398
|
{onReply && (
|
|
359
399
|
<button
|
|
@@ -405,6 +445,108 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
405
445
|
onClose={() => setShowTransferPicker(false)}
|
|
406
446
|
/>
|
|
407
447
|
)}
|
|
448
|
+
{/* Moderation menu button */}
|
|
449
|
+
{!isUser && (onDeleteMessage || onMuteAgent || onSetRole) && (
|
|
450
|
+
<button
|
|
451
|
+
onClick={() => setShowModMenu(!showModMenu)}
|
|
452
|
+
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"
|
|
453
|
+
title="Moderate"
|
|
454
|
+
>
|
|
455
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
|
|
456
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
457
|
+
</svg>
|
|
458
|
+
</button>
|
|
459
|
+
)}
|
|
460
|
+
{showModMenu && !isUser && (
|
|
461
|
+
<div className="absolute right-0 top-7 z-50 bg-[#1a1a2e] border border-white/[0.1] rounded-[8px] shadow-lg py-1 min-w-[160px]">
|
|
462
|
+
{onDeleteMessage && (
|
|
463
|
+
<button
|
|
464
|
+
onClick={() => {
|
|
465
|
+
onDeleteMessage(message.id, message.senderId)
|
|
466
|
+
setShowModMenu(false)
|
|
467
|
+
}}
|
|
468
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-red-400 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
469
|
+
style={{ fontFamily: 'inherit' }}
|
|
470
|
+
>
|
|
471
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
472
|
+
<polyline points="3 6 5 6 21 6" />
|
|
473
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
474
|
+
</svg>
|
|
475
|
+
Delete message
|
|
476
|
+
</button>
|
|
477
|
+
)}
|
|
478
|
+
{onMuteAgent && onUnmuteAgent && (() => {
|
|
479
|
+
const muted = isAgentMutedInChatroom(chatroom, message.senderId)
|
|
480
|
+
return muted ? (
|
|
481
|
+
<button
|
|
482
|
+
onClick={() => {
|
|
483
|
+
onUnmuteAgent(message.senderId)
|
|
484
|
+
setShowModMenu(false)
|
|
485
|
+
}}
|
|
486
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-green-400 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
487
|
+
style={{ fontFamily: 'inherit' }}
|
|
488
|
+
>
|
|
489
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
490
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
491
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
492
|
+
</svg>
|
|
493
|
+
Unmute agent
|
|
494
|
+
</button>
|
|
495
|
+
) : (
|
|
496
|
+
<button
|
|
497
|
+
onClick={() => {
|
|
498
|
+
onMuteAgent(message.senderId)
|
|
499
|
+
setShowModMenu(false)
|
|
500
|
+
}}
|
|
501
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-amber-400 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
502
|
+
style={{ fontFamily: 'inherit' }}
|
|
503
|
+
>
|
|
504
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
505
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
506
|
+
<line x1="23" y1="9" x2="17" y2="15" />
|
|
507
|
+
<line x1="17" y1="9" x2="23" y2="15" />
|
|
508
|
+
</svg>
|
|
509
|
+
Mute 30 min
|
|
510
|
+
</button>
|
|
511
|
+
)
|
|
512
|
+
})()}
|
|
513
|
+
{onSetRole && (() => {
|
|
514
|
+
const currentRole = getMemberRoleFromChatroom(chatroom, message.senderId)
|
|
515
|
+
const roleOptions: Array<{ value: 'admin' | 'moderator' | 'member'; label: string }> = [
|
|
516
|
+
{ value: 'admin', label: 'Set Admin' },
|
|
517
|
+
{ value: 'moderator', label: 'Set Moderator' },
|
|
518
|
+
{ value: 'member', label: 'Set Member' },
|
|
519
|
+
]
|
|
520
|
+
return roleOptions
|
|
521
|
+
.filter((opt) => opt.value !== currentRole)
|
|
522
|
+
.map((opt) => (
|
|
523
|
+
<button
|
|
524
|
+
key={opt.value}
|
|
525
|
+
onClick={() => {
|
|
526
|
+
onSetRole(message.senderId, opt.value)
|
|
527
|
+
setShowModMenu(false)
|
|
528
|
+
}}
|
|
529
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-text-2 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
530
|
+
style={{ fontFamily: 'inherit' }}
|
|
531
|
+
>
|
|
532
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
533
|
+
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
534
|
+
<circle cx="8.5" cy="7" r="4" />
|
|
535
|
+
<polyline points="17 11 19 13 23 9" />
|
|
536
|
+
</svg>
|
|
537
|
+
{opt.label}
|
|
538
|
+
</button>
|
|
539
|
+
))
|
|
540
|
+
})()}
|
|
541
|
+
<button
|
|
542
|
+
onClick={() => setShowModMenu(false)}
|
|
543
|
+
className="w-full px-3 py-1.5 text-[11px] text-text-3 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left border-t border-white/[0.06] mt-1"
|
|
544
|
+
style={{ fontFamily: 'inherit' }}
|
|
545
|
+
>
|
|
546
|
+
Cancel
|
|
547
|
+
</button>
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
408
550
|
{/* Reaction button */}
|
|
409
551
|
<button
|
|
410
552
|
onClick={() => setShowPicker(!showPicker)}
|
|
@@ -6,9 +6,145 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
6
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
7
|
import { toast } from 'sonner'
|
|
8
8
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
-
import type { Agent } from '@/types'
|
|
9
|
+
import type { Agent, ChatroomRoutingRule } from '@/types'
|
|
10
10
|
import { CheckIcon } from '@/components/shared/check-icon'
|
|
11
11
|
|
|
12
|
+
function genRuleId(): string {
|
|
13
|
+
return `rule-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RuleFormState {
|
|
17
|
+
type: 'keyword' | 'capability'
|
|
18
|
+
pattern: string
|
|
19
|
+
keywords: string
|
|
20
|
+
agentId: string
|
|
21
|
+
priority: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const emptyRuleForm: RuleFormState = {
|
|
25
|
+
type: 'keyword',
|
|
26
|
+
pattern: '',
|
|
27
|
+
keywords: '',
|
|
28
|
+
agentId: '',
|
|
29
|
+
priority: 10,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function RoutingRuleForm({
|
|
33
|
+
rule,
|
|
34
|
+
memberAgents,
|
|
35
|
+
onSave,
|
|
36
|
+
onCancel,
|
|
37
|
+
}: {
|
|
38
|
+
rule: RuleFormState
|
|
39
|
+
memberAgents: Agent[]
|
|
40
|
+
onSave: (form: RuleFormState) => void
|
|
41
|
+
onCancel: () => void
|
|
42
|
+
}) {
|
|
43
|
+
const [form, setForm] = useState<RuleFormState>(rule)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="p-3 rounded-[8px] bg-white/[0.04] border border-white/[0.08] space-y-3">
|
|
47
|
+
<div className="flex gap-2">
|
|
48
|
+
{(['keyword', 'capability'] as const).map((t) => (
|
|
49
|
+
<button
|
|
50
|
+
key={t}
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={() => setForm((f) => ({ ...f, type: t }))}
|
|
53
|
+
className={`flex-1 py-1.5 text-[11px] font-600 capitalize rounded-[6px] cursor-pointer transition-all ${
|
|
54
|
+
form.type === t
|
|
55
|
+
? 'bg-accent-soft text-accent-bright'
|
|
56
|
+
: 'bg-white/[0.04] text-text-3 hover:text-text-2'
|
|
57
|
+
}`}
|
|
58
|
+
>
|
|
59
|
+
{t}
|
|
60
|
+
</button>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{form.type === 'keyword' && (
|
|
65
|
+
<>
|
|
66
|
+
<div>
|
|
67
|
+
<label className="block text-[11px] font-600 text-text-3 mb-1">Keywords (comma-separated)</label>
|
|
68
|
+
<input
|
|
69
|
+
type="text"
|
|
70
|
+
value={form.keywords}
|
|
71
|
+
onChange={(e) => setForm((f) => ({ ...f, keywords: e.target.value }))}
|
|
72
|
+
placeholder="e.g. deploy, devops, infrastructure"
|
|
73
|
+
className="w-full px-2.5 py-1.5 rounded-[6px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<div>
|
|
77
|
+
<label className="block text-[11px] font-600 text-text-3 mb-1">Regex pattern (optional)</label>
|
|
78
|
+
<input
|
|
79
|
+
type="text"
|
|
80
|
+
value={form.pattern}
|
|
81
|
+
onChange={(e) => setForm((f) => ({ ...f, pattern: e.target.value }))}
|
|
82
|
+
placeholder="e.g. deploy|release|ship"
|
|
83
|
+
className="w-full px-2.5 py-1.5 rounded-[6px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{form.type === 'capability' && (
|
|
90
|
+
<div>
|
|
91
|
+
<label className="block text-[11px] font-600 text-text-3 mb-1">Capability pattern</label>
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
value={form.pattern}
|
|
95
|
+
onChange={(e) => setForm((f) => ({ ...f, pattern: e.target.value }))}
|
|
96
|
+
placeholder="e.g. frontend, research, devops"
|
|
97
|
+
className="w-full px-2.5 py-1.5 rounded-[6px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<div className="flex gap-2">
|
|
103
|
+
<div className="flex-1">
|
|
104
|
+
<label className="block text-[11px] font-600 text-text-3 mb-1">Route to agent</label>
|
|
105
|
+
<select
|
|
106
|
+
value={form.agentId}
|
|
107
|
+
onChange={(e) => setForm((f) => ({ ...f, agentId: e.target.value }))}
|
|
108
|
+
className="w-full px-2.5 py-1.5 rounded-[6px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text focus:outline-none focus:border-accent-bright/40"
|
|
109
|
+
>
|
|
110
|
+
<option value="">Select agent...</option>
|
|
111
|
+
{memberAgents.map((a) => (
|
|
112
|
+
<option key={a.id} value={a.id}>{a.name}</option>
|
|
113
|
+
))}
|
|
114
|
+
</select>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="w-20">
|
|
117
|
+
<label className="block text-[11px] font-600 text-text-3 mb-1">Priority</label>
|
|
118
|
+
<input
|
|
119
|
+
type="number"
|
|
120
|
+
value={form.priority}
|
|
121
|
+
onChange={(e) => setForm((f) => ({ ...f, priority: parseInt(e.target.value, 10) || 0 }))}
|
|
122
|
+
className="w-full px-2.5 py-1.5 rounded-[6px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text focus:outline-none focus:border-accent-bright/40"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="flex gap-2 justify-end">
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={onCancel}
|
|
131
|
+
className="px-3 py-1.5 text-[11px] font-600 text-text-3 hover:text-text-2 cursor-pointer"
|
|
132
|
+
>
|
|
133
|
+
Cancel
|
|
134
|
+
</button>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => onSave(form)}
|
|
138
|
+
disabled={!form.agentId || (form.type === 'keyword' && !form.keywords.trim() && !form.pattern.trim()) || (form.type === 'capability' && !form.pattern.trim())}
|
|
139
|
+
className="px-3 py-1.5 text-[11px] font-600 bg-accent-bright text-white rounded-[6px] hover:bg-accent-bright/90 disabled:opacity-50 cursor-pointer"
|
|
140
|
+
>
|
|
141
|
+
Save Rule
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
12
148
|
export function ChatroomSheet() {
|
|
13
149
|
const open = useChatroomStore((s) => s.chatroomSheetOpen)
|
|
14
150
|
const editingId = useChatroomStore((s) => s.editingChatroomId)
|
|
@@ -25,7 +161,10 @@ export function ChatroomSheet() {
|
|
|
25
161
|
const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([])
|
|
26
162
|
const [chatMode, setChatMode] = useState<'sequential' | 'parallel'>('sequential')
|
|
27
163
|
const [autoAddress, setAutoAddress] = useState(false)
|
|
164
|
+
const [routingRules, setRoutingRules] = useState<ChatroomRoutingRule[]>([])
|
|
28
165
|
const [saving, setSaving] = useState(false)
|
|
166
|
+
const [addingRule, setAddingRule] = useState(false)
|
|
167
|
+
const [editingRuleId, setEditingRuleId] = useState<string | null>(null)
|
|
29
168
|
|
|
30
169
|
const editing = editingId ? chatrooms[editingId] : null
|
|
31
170
|
|
|
@@ -36,24 +175,36 @@ export function ChatroomSheet() {
|
|
|
36
175
|
setSelectedAgentIds([...editing.agentIds])
|
|
37
176
|
setChatMode(editing.chatMode || 'sequential')
|
|
38
177
|
setAutoAddress(editing.autoAddress || false)
|
|
178
|
+
setRoutingRules([...(editing.routingRules || [])])
|
|
39
179
|
} else {
|
|
40
180
|
setName('')
|
|
41
181
|
setDescription('')
|
|
42
182
|
setSelectedAgentIds([])
|
|
43
183
|
setChatMode('sequential')
|
|
44
184
|
setAutoAddress(false)
|
|
185
|
+
setRoutingRules([])
|
|
45
186
|
}
|
|
187
|
+
setAddingRule(false)
|
|
188
|
+
setEditingRuleId(null)
|
|
46
189
|
}, [editing, open])
|
|
47
190
|
|
|
48
191
|
const handleSave = async () => {
|
|
49
192
|
if (!name.trim() || saving) return
|
|
50
193
|
setSaving(true)
|
|
51
194
|
try {
|
|
195
|
+
const payload = {
|
|
196
|
+
name,
|
|
197
|
+
description,
|
|
198
|
+
agentIds: selectedAgentIds,
|
|
199
|
+
chatMode,
|
|
200
|
+
autoAddress,
|
|
201
|
+
routingRules: routingRules.length > 0 ? routingRules : undefined,
|
|
202
|
+
}
|
|
52
203
|
if (editing) {
|
|
53
|
-
await updateChatroom(editing.id,
|
|
204
|
+
await updateChatroom(editing.id, payload)
|
|
54
205
|
toast.success('Chatroom saved')
|
|
55
206
|
} else {
|
|
56
|
-
const chatroom = await createChatroom(
|
|
207
|
+
const chatroom = await createChatroom(payload)
|
|
57
208
|
setCurrentChatroom(chatroom.id)
|
|
58
209
|
toast.success('Chatroom created')
|
|
59
210
|
}
|
|
@@ -81,10 +232,53 @@ export function ChatroomSheet() {
|
|
|
81
232
|
)
|
|
82
233
|
}
|
|
83
234
|
|
|
235
|
+
const handleAddRule = (form: RuleFormState) => {
|
|
236
|
+
const rule: ChatroomRoutingRule = {
|
|
237
|
+
id: genRuleId(),
|
|
238
|
+
type: form.type,
|
|
239
|
+
agentId: form.agentId,
|
|
240
|
+
priority: form.priority,
|
|
241
|
+
...(form.pattern.trim() ? { pattern: form.pattern.trim() } : {}),
|
|
242
|
+
...(form.type === 'keyword' && form.keywords.trim()
|
|
243
|
+
? { keywords: form.keywords.split(',').map((k) => k.trim()).filter(Boolean) }
|
|
244
|
+
: {}),
|
|
245
|
+
}
|
|
246
|
+
setRoutingRules((prev) => [...prev, rule].sort((a, b) => a.priority - b.priority))
|
|
247
|
+
setAddingRule(false)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const handleEditRule = (form: RuleFormState) => {
|
|
251
|
+
setRoutingRules((prev) =>
|
|
252
|
+
prev.map((r) =>
|
|
253
|
+
r.id === editingRuleId
|
|
254
|
+
? {
|
|
255
|
+
...r,
|
|
256
|
+
type: form.type,
|
|
257
|
+
agentId: form.agentId,
|
|
258
|
+
priority: form.priority,
|
|
259
|
+
pattern: form.pattern.trim() || undefined,
|
|
260
|
+
keywords:
|
|
261
|
+
form.type === 'keyword' && form.keywords.trim()
|
|
262
|
+
? form.keywords.split(',').map((k) => k.trim()).filter(Boolean)
|
|
263
|
+
: undefined,
|
|
264
|
+
}
|
|
265
|
+
: r,
|
|
266
|
+
).sort((a, b) => a.priority - b.priority),
|
|
267
|
+
)
|
|
268
|
+
setEditingRuleId(null)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const removeRule = (ruleId: string) => {
|
|
272
|
+
setRoutingRules((prev) => prev.filter((r) => r.id !== ruleId))
|
|
273
|
+
}
|
|
274
|
+
|
|
84
275
|
const agentList = Object.values(agents).filter(
|
|
85
276
|
(a: Agent) => !a.trashedAt
|
|
86
277
|
) as Agent[]
|
|
87
278
|
|
|
279
|
+
const memberAgents = agentList.filter((a) => selectedAgentIds.includes(a.id))
|
|
280
|
+
const sortedRules = [...routingRules].sort((a, b) => a.priority - b.priority)
|
|
281
|
+
|
|
88
282
|
return (
|
|
89
283
|
<BottomSheet open={open} onClose={() => setChatroomSheetOpen(false)}>
|
|
90
284
|
<div className="p-6 max-w-[560px] mx-auto">
|
|
@@ -178,7 +372,7 @@ export function ChatroomSheet() {
|
|
|
178
372
|
selected ? 'bg-accent-soft/40' : 'hover:bg-white/[0.04]'
|
|
179
373
|
}`}
|
|
180
374
|
>
|
|
181
|
-
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
|
|
375
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
|
|
182
376
|
<span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
|
|
183
377
|
{selected && (
|
|
184
378
|
<CheckIcon size={14} className="text-accent-bright shrink-0" />
|
|
@@ -189,6 +383,97 @@ export function ChatroomSheet() {
|
|
|
189
383
|
)}
|
|
190
384
|
</div>
|
|
191
385
|
</div>
|
|
386
|
+
|
|
387
|
+
{/* Routing Rules */}
|
|
388
|
+
<div>
|
|
389
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">
|
|
390
|
+
Routing Rules ({sortedRules.length})
|
|
391
|
+
</label>
|
|
392
|
+
<p className="text-[11px] text-text-3 mb-2">
|
|
393
|
+
Route messages to specific agents based on keywords or capabilities. Evaluated before auto-address.
|
|
394
|
+
</p>
|
|
395
|
+
|
|
396
|
+
{sortedRules.length > 0 && (
|
|
397
|
+
<div className="space-y-2 mb-2">
|
|
398
|
+
{sortedRules.map((rule) => {
|
|
399
|
+
const agent = agents[rule.agentId]
|
|
400
|
+
if (editingRuleId === rule.id) {
|
|
401
|
+
return (
|
|
402
|
+
<RoutingRuleForm
|
|
403
|
+
key={rule.id}
|
|
404
|
+
rule={{
|
|
405
|
+
type: rule.type,
|
|
406
|
+
pattern: rule.pattern || '',
|
|
407
|
+
keywords: rule.keywords?.join(', ') || '',
|
|
408
|
+
agentId: rule.agentId,
|
|
409
|
+
priority: rule.priority,
|
|
410
|
+
}}
|
|
411
|
+
memberAgents={memberAgents}
|
|
412
|
+
onSave={handleEditRule}
|
|
413
|
+
onCancel={() => setEditingRuleId(null)}
|
|
414
|
+
/>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
return (
|
|
418
|
+
<div
|
|
419
|
+
key={rule.id}
|
|
420
|
+
className="flex items-center gap-2 px-3 py-2 rounded-[8px] bg-white/[0.04] border border-white/[0.08]"
|
|
421
|
+
>
|
|
422
|
+
<span className="text-[10px] font-700 text-text-3 bg-white/[0.06] px-1.5 py-0.5 rounded">
|
|
423
|
+
P{rule.priority}
|
|
424
|
+
</span>
|
|
425
|
+
<span className="text-[10px] font-600 text-accent-bright/70 uppercase">
|
|
426
|
+
{rule.type}
|
|
427
|
+
</span>
|
|
428
|
+
<span className="text-[12px] text-text-2 flex-1 truncate">
|
|
429
|
+
{rule.type === 'keyword'
|
|
430
|
+
? (rule.keywords?.join(', ') || rule.pattern || '(no match)')
|
|
431
|
+
: (rule.pattern || '(no pattern)')}
|
|
432
|
+
</span>
|
|
433
|
+
<span className="text-[11px] text-text-3 truncate max-w-[100px]">
|
|
434
|
+
{agent?.name || 'Unknown'}
|
|
435
|
+
</span>
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
onClick={() => setEditingRuleId(rule.id)}
|
|
439
|
+
className="text-[11px] text-text-3 hover:text-text-2 cursor-pointer px-1"
|
|
440
|
+
>
|
|
441
|
+
Edit
|
|
442
|
+
</button>
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
onClick={() => removeRule(rule.id)}
|
|
446
|
+
className="text-[11px] text-red-400 hover:text-red-300 cursor-pointer px-1"
|
|
447
|
+
>
|
|
448
|
+
Remove
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
)
|
|
452
|
+
})}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{addingRule ? (
|
|
457
|
+
<RoutingRuleForm
|
|
458
|
+
rule={emptyRuleForm}
|
|
459
|
+
memberAgents={memberAgents}
|
|
460
|
+
onSave={handleAddRule}
|
|
461
|
+
onCancel={() => setAddingRule(false)}
|
|
462
|
+
/>
|
|
463
|
+
) : (
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
onClick={() => setAddingRule(true)}
|
|
467
|
+
disabled={memberAgents.length === 0}
|
|
468
|
+
className="w-full py-2 rounded-[8px] border border-dashed border-white/[0.12] text-[12px] font-600 text-text-3 hover:text-text-2 hover:border-white/[0.2] cursor-pointer transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
469
|
+
>
|
|
470
|
+
+ Add Rule
|
|
471
|
+
</button>
|
|
472
|
+
)}
|
|
473
|
+
{memberAgents.length === 0 && (
|
|
474
|
+
<p className="text-[11px] text-text-3 mt-1">Add members first to create routing rules.</p>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
192
477
|
</div>
|
|
193
478
|
|
|
194
479
|
<div className="flex items-center gap-3 mt-6">
|