@swarmclawai/swarmclaw 0.6.6 → 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 +57 -27
- package/package.json +6 -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 +17 -1
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +19 -1
- package/src/app/api/chatrooms/route.ts +12 -2
- 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/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 +20 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/cli/index.js +5 -0
- package/src/cli/index.ts +223 -39
- package/src/components/agents/agent-card.tsx +37 -6
- package/src/components/agents/agent-chat-list.tsx +78 -2
- package/src/components/agents/agent-sheet.tsx +79 -0
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +22 -7
- package/src/components/chat/message-bubble.tsx +14 -14
- package/src/components/chat/message-list.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +164 -22
- package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +23 -2
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/layout/app-layout.tsx +17 -1
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +91 -16
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- 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/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +41 -1
- package/src/lib/server/chatroom-helpers.ts +22 -1
- 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/manager.ts +159 -3
- 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.ts +9 -0
- 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/queue.ts +12 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +22 -3
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/storage.ts +120 -6
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/stores/use-app-store.ts +7 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +38 -1
- package/tsconfig.json +2 -1
|
@@ -324,17 +324,32 @@ export function ChatArea() {
|
|
|
324
324
|
<DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
|
|
325
325
|
|
|
326
326
|
{messagesLoading && !messages.length ? (
|
|
327
|
-
<div className="flex-1 flex
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
327
|
+
<div className="flex-1 flex flex-col gap-5 px-4 md:px-12 lg:px-16 py-8" style={{ animation: 'fade-in 0.2s ease' }}>
|
|
328
|
+
{/* Skeleton message bubbles */}
|
|
329
|
+
<div className="flex gap-3 max-w-[70%]">
|
|
330
|
+
<div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
|
|
331
|
+
<div className="flex-1 space-y-2">
|
|
332
|
+
<div className="h-3 w-24 rounded bg-white/[0.06] animate-pulse" />
|
|
333
|
+
<div className="h-16 rounded-[12px] bg-white/[0.04] animate-pulse" />
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div className="flex gap-3 max-w-[60%] self-end flex-row-reverse">
|
|
337
|
+
<div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
|
|
338
|
+
<div className="flex-1 space-y-2">
|
|
339
|
+
<div className="h-3 w-16 rounded bg-white/[0.06] animate-pulse ml-auto" />
|
|
340
|
+
<div className="h-10 rounded-[12px] bg-white/[0.04] animate-pulse" />
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div className="flex gap-3 max-w-[65%]">
|
|
344
|
+
<div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
|
|
345
|
+
<div className="flex-1 space-y-2">
|
|
346
|
+
<div className="h-3 w-20 rounded bg-white/[0.06] animate-pulse" />
|
|
347
|
+
<div className="h-24 rounded-[12px] bg-white/[0.04] animate-pulse" />
|
|
332
348
|
</div>
|
|
333
|
-
<span className="text-[13px] text-text-3/50 font-500">Loading messages...</span>
|
|
334
349
|
</div>
|
|
335
350
|
</div>
|
|
336
351
|
) : isEmpty ? (
|
|
337
|
-
<div className="flex-1 flex flex-col items-center justify-center px-6 pb-4 relative">
|
|
352
|
+
<div className="flex-1 flex flex-col items-center justify-center px-6 pb-[120px] md:pb-4 relative">
|
|
338
353
|
{/* Atmospheric background glow */}
|
|
339
354
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
|
340
355
|
<div className="absolute top-[20%] left-[50%] -translate-x-1/2 w-[500px] h-[300px]"
|
|
@@ -516,7 +516,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
516
516
|
)}
|
|
517
517
|
</div>
|
|
518
518
|
) : (
|
|
519
|
-
<div className={`msg-content text-[15px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
|
|
519
|
+
<div className={`msg-content text-[15px] md:text-[14px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
|
|
520
520
|
<ReactMarkdown
|
|
521
521
|
remarkPlugins={[remarkGfm]}
|
|
522
522
|
rehypePlugins={[rehypeHighlight]}
|
|
@@ -660,12 +660,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
660
660
|
)}
|
|
661
661
|
|
|
662
662
|
{/* Action buttons */}
|
|
663
|
-
<div className={`flex items-center gap-1 mt-1.5 px-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
|
|
663
|
+
<div className={`flex items-center gap-1 mt-1.5 px-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
|
|
664
664
|
<button
|
|
665
665
|
onClick={handleCopy}
|
|
666
666
|
aria-label="Copy message"
|
|
667
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
668
|
-
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
667
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
|
|
668
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
|
|
669
669
|
style={{ fontFamily: 'inherit' }}
|
|
670
670
|
>
|
|
671
671
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -678,8 +678,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
678
678
|
<button
|
|
679
679
|
onClick={() => onToggleBookmark(messageIndex)}
|
|
680
680
|
aria-label={message.bookmarked ? 'Remove bookmark' : 'Bookmark message'}
|
|
681
|
-
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
682
|
-
text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all ${message.bookmarked ? 'text-amber-400' : ''}`}
|
|
681
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
|
|
682
|
+
text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all justify-center md:justify-start ${message.bookmarked ? 'text-amber-400' : ''}`}
|
|
683
683
|
style={{ fontFamily: 'inherit' }}
|
|
684
684
|
>
|
|
685
685
|
<svg width="12" height="12" viewBox="0 0 24 24" fill={message.bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -692,8 +692,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
692
692
|
<button
|
|
693
693
|
onClick={() => { setEditText(message.text); setEditing(true) }}
|
|
694
694
|
aria-label="Edit and resend"
|
|
695
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
696
|
-
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
695
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
|
|
696
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
|
|
697
697
|
style={{ fontFamily: 'inherit' }}
|
|
698
698
|
>
|
|
699
699
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -707,8 +707,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
707
707
|
<button
|
|
708
708
|
onClick={() => onFork(messageIndex)}
|
|
709
709
|
aria-label="Fork conversation from here"
|
|
710
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
711
|
-
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
710
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
|
|
711
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
|
|
712
712
|
style={{ fontFamily: 'inherit' }}
|
|
713
713
|
>
|
|
714
714
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -725,8 +725,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
725
725
|
<button
|
|
726
726
|
onClick={onRetry}
|
|
727
727
|
aria-label="Retry message"
|
|
728
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
729
|
-
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
728
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
|
|
729
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
|
|
730
730
|
style={{ fontFamily: 'inherit' }}
|
|
731
731
|
>
|
|
732
732
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -741,8 +741,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
741
741
|
<button
|
|
742
742
|
onClick={() => setTransferPickerOpen(!transferPickerOpen)}
|
|
743
743
|
aria-label="Transfer to another agent"
|
|
744
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
745
|
-
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
744
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
|
|
745
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
|
|
746
746
|
style={{ fontFamily: 'inherit' }}
|
|
747
747
|
>
|
|
748
748
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -397,7 +397,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
397
397
|
<div
|
|
398
398
|
ref={scrollRef}
|
|
399
399
|
onScroll={updateScrollState}
|
|
400
|
-
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-
|
|
400
|
+
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 md:px-12 lg:px-16 pt-6 pb-[120px] md:pb-10 fade-up"
|
|
401
401
|
>
|
|
402
402
|
<div className="flex flex-col gap-6 relative">
|
|
403
403
|
{/* Chat spine — vertical line for assistant messages */}
|
|
@@ -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
|
|
|
@@ -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)}
|