diff --git a/src/langbot/pkg/pipeline/monitoring_helper.py b/src/langbot/pkg/pipeline/monitoring_helper.py index 19467cc84..a3a9654bc 100644 --- a/src/langbot/pkg/pipeline/monitoring_helper.py +++ b/src/langbot/pkg/pipeline/monitoring_helper.py @@ -32,7 +32,7 @@ class MonitoringHelper: """Record the start of query processing, returns message_id""" try: # Check if session exists, if not, record session start - session_id = f'{query.launcher_type}_{query.launcher_id}' + session_id = f'{query.launcher_type.value if hasattr(query.launcher_type, "value") else query.launcher_type}_{query.launcher_id}' # Get sender name from message event sender_name = None @@ -137,7 +137,7 @@ class MonitoringHelper: ): """Record bot response message to monitoring""" try: - session_id = f'{query.launcher_type}_{query.launcher_id}' + session_id = f'{query.launcher_type.value if hasattr(query.launcher_type, "value") else query.launcher_type}_{query.launcher_id}' # Get sender name from message event sender_name = None @@ -202,7 +202,7 @@ class MonitoringHelper: ) -> str: """Record query processing error, returns message_id""" try: - session_id = f'{query.launcher_type}_{query.launcher_id}' + session_id = f'{query.launcher_type.value if hasattr(query.launcher_type, "value") else query.launcher_type}_{query.launcher_id}' # Get sender name from message event sender_name = None @@ -268,7 +268,7 @@ class MonitoringHelper: ): """Record LLM call""" try: - session_id = f'{query.launcher_type}_{query.launcher_id}' + session_id = f'{query.launcher_type.value if hasattr(query.launcher_type, "value") else query.launcher_type}_{query.launcher_id}' await ap.monitoring_service.record_llm_call( bot_id=bot_id, diff --git a/src/langbot/pkg/pipeline/pipelinemgr.py b/src/langbot/pkg/pipeline/pipelinemgr.py index 1426fe3de..cdd44df89 100644 --- a/src/langbot/pkg/pipeline/pipelinemgr.py +++ b/src/langbot/pkg/pipeline/pipelinemgr.py @@ -178,7 +178,7 @@ class RuntimePipeline: bot_name = query.variables.get('_monitoring_bot_name', 'Unknown') pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown') message_id = query.variables.get('_monitoring_message_id', '') - session_id = f'{query.launcher_type}_{query.launcher_id}' + session_id = f'{query.launcher_type.value if hasattr(query.launcher_type, "value") else query.launcher_type}_{query.launcher_id}' # Update message status to error if message_id: diff --git a/web/src/app/home/bots/BotDetailContent.tsx b/web/src/app/home/bots/BotDetailContent.tsx index df197dc50..8440e47ac 100644 --- a/web/src/app/home/bots/BotDetailContent.tsx +++ b/web/src/app/home/bots/BotDetailContent.tsx @@ -22,19 +22,11 @@ import { import BotForm from '@/app/home/bots/components/bot-form/BotForm'; import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent'; import BotSessionMonitor from '@/app/home/bots/components/bot-session/BotSessionMonitor'; -import BotAdminsPanel from '@/app/home/bots/components/bot-admins/BotAdminsPanel'; import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-session/BotSessionMonitor'; import { httpClient } from '@/app/infra/http/HttpClient'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useTranslation } from 'react-i18next'; -import { - Settings, - FileText, - Users, - RefreshCw, - Trash2, - ShieldCheck, -} from 'lucide-react'; +import { Settings, FileText, Users, RefreshCw, Trash2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; @@ -237,10 +229,6 @@ export default function BotDetailContent({ id }: { id: string }) { )} - - - {t('bots.admins.title')} - {/* Tab: Configuration */} @@ -303,16 +291,6 @@ export default function BotDetailContent({ id }: { id: string }) { - - {/* Tab: Admins */} - -
- -
-
diff --git a/web/src/app/home/bots/components/bot-admins/BotAdminsDialog.tsx b/web/src/app/home/bots/components/bot-admins/BotAdminsDialog.tsx new file mode 100644 index 000000000..8c187db32 --- /dev/null +++ b/web/src/app/home/bots/components/bot-admins/BotAdminsDialog.tsx @@ -0,0 +1,199 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Trash2, Plus, ShieldCheck } from 'lucide-react'; +import { toast } from 'sonner'; + +export interface BotAdmin { + id: number; + launcher_type: string; + launcher_id: string; +} + +interface BotAdminsDialogProps { + botId: string; + open: boolean; + onOpenChange: (open: boolean) => void; + admins: BotAdmin[]; + onAdminsChange: () => void; +} + +export default function BotAdminsDialog({ + botId, + open, + onOpenChange, + admins, + onAdminsChange, +}: BotAdminsDialogProps) { + const { t } = useTranslation(); + const [newType, setNewType] = useState('person'); + const [newId, setNewId] = useState(''); + const [adding, setAdding] = useState(false); + + async function handleAdd() { + if (!newId.trim()) return; + setAdding(true); + try { + await httpClient.addBotAdmin(botId, newType, newId.trim()); + toast.success(t('bots.admins.addSuccess')); + setNewId(''); + onAdminsChange(); + } catch (e: unknown) { + const err = e as { msg?: string; message?: string }; + toast.error(t('bots.admins.addError') + (err?.msg ?? err?.message ?? '')); + } finally { + setAdding(false); + } + } + + async function handleDelete(id: number) { + try { + await httpClient.deleteBotAdmin(botId, id); + toast.success(t('bots.admins.deleteSuccess')); + onAdminsChange(); + } catch (e: unknown) { + const err = e as { msg?: string; message?: string }; + toast.error( + t('bots.admins.deleteError') + (err?.msg ?? err?.message ?? ''), + ); + } + } + + return ( + + + + + + {t('bots.admins.title')} + + {t('bots.admins.description')} + + +
+ {/* Add row */} +
+ + setNewId(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + /> + +
+ + {/* List */} + {admins.length === 0 ? ( +
+ {t('bots.admins.noAdmins')} +
+ ) : ( + +
+ + + + + + + + + {admins.map((admin) => ( + + + + + + ))} + +
+ {t('bots.admins.launcherType')} + + {t('bots.admins.launcherId')} + +
+ + {admin.launcher_type === 'person' + ? t('bots.admins.typePerson') + : t('bots.admins.typeGroup')} + + + {admin.launcher_id} + + +
+
+
+ )} +
+
+
+ ); +} + +// Shared hook so the session monitor and the dialog stay in sync. +export function useBotAdmins(botId: string) { + const [admins, setAdmins] = useState([]); + + const reload = useCallback(async () => { + try { + const res = await httpClient.getBotAdmins(botId); + setAdmins(res.admins ?? []); + } catch (error) { + console.error('Failed to load bot admins:', error); + } + }, [botId]); + + useEffect(() => { + reload(); + }, [reload]); + + return { admins, reload }; +} diff --git a/web/src/app/home/bots/components/bot-admins/BotAdminsPanel.tsx b/web/src/app/home/bots/components/bot-admins/BotAdminsPanel.tsx deleted file mode 100644 index 55fbd6a6c..000000000 --- a/web/src/app/home/bots/components/bot-admins/BotAdminsPanel.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { httpClient } from '@/app/infra/http/HttpClient'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { Trash2, Plus } from 'lucide-react'; -import { toast } from 'sonner'; - -interface BotAdmin { - id: number; - launcher_type: string; - launcher_id: string; -} - -export default function BotAdminsPanel({ botId }: { botId: string }) { - const { t } = useTranslation(); - const [admins, setAdmins] = useState([]); - const [loading, setLoading] = useState(false); - const [newType, setNewType] = useState('person'); - const [newId, setNewId] = useState(''); - const [adding, setAdding] = useState(false); - - const load = useCallback(async () => { - setLoading(true); - try { - const res = await httpClient.getBotAdmins(botId); - setAdmins(res.admins ?? []); - } finally { - setLoading(false); - } - }, [botId]); - - useEffect(() => { - load(); - }, [load]); - - async function handleAdd() { - if (!newId.trim()) return; - setAdding(true); - try { - await httpClient.addBotAdmin(botId, newType, newId.trim()); - toast.success(t('bots.admins.addSuccess')); - setNewId(''); - await load(); - } catch (e: any) { - toast.error(t('bots.admins.addError') + (e?.msg ?? e?.message ?? '')); - } finally { - setAdding(false); - } - } - - async function handleDelete(id: number) { - try { - await httpClient.deleteBotAdmin(botId, id); - toast.success(t('bots.admins.deleteSuccess')); - setAdmins((prev) => prev.filter((a) => a.id !== id)); - } catch (e: any) { - toast.error(t('bots.admins.deleteError') + (e?.msg ?? e?.message ?? '')); - } - } - - return ( -
-
- - setNewId(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleAdd()} - /> - -
- - {loading ? ( -
- {t('bots.sessionMonitor.loading')} -
- ) : admins.length === 0 ? ( -
- {t('bots.admins.noAdmins')} -
- ) : ( -
- - - - - - - - - {admins.map((admin) => ( - - - - - - ))} - -
- {t('bots.admins.launcherType')} - - {t('bots.admins.launcherId')} - -
- - {admin.launcher_type === 'person' - ? t('bots.admins.typePerson') - : t('bots.admins.typeGroup')} - - {admin.launcher_id} - -
-
- )} -
- ); -} diff --git a/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx b/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx index e38da3893..25942394e 100644 --- a/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx +++ b/web/src/app/home/bots/components/bot-session/BotSessionMonitor.tsx @@ -18,7 +18,14 @@ import { Workflow, ThumbsUp, ThumbsDown, + ShieldCheck, + ShieldOff, } from 'lucide-react'; +import { toast } from 'sonner'; +import BotAdminsDialog, { + useBotAdmins, +} from '@/app/home/bots/components/bot-admins/BotAdminsDialog'; +import type { BotAdmin } from '@/app/home/bots/components/bot-admins/BotAdminsDialog'; import { copyToClipboard } from '@/app/utils/clipboard'; import { MessageChainComponent, @@ -94,15 +101,60 @@ const BotSessionMonitor = forwardRef< Record >({}); const messagesContainerRef = useRef(null); + const { admins, reload: reloadAdmins } = useBotAdmins(botId); + const [adminsDialogOpen, setAdminsDialogOpen] = useState(false); + const [togglingAdmin, setTogglingAdmin] = useState(null); const parseSessionType = (sessionId: string): string | null => { - const idx = sessionId.indexOf('_'); - if (idx === -1) return null; - const type = sessionId.slice(0, idx); - if (type === 'person' || type === 'group') return type; + const lower = sessionId.toLowerCase(); + if (lower.includes('person')) return 'person'; + if (lower.includes('group')) return 'group'; return null; }; + const isSessionAdmin = (session: SessionInfo): boolean => { + const type = parseSessionType(session.session_id); + const lid = + session.user_id ?? + session.session_id.replace( + /^.*?[._](?:PERSON|GROUP|person|group)[._]/i, + '', + ); + return admins.some( + (a: BotAdmin) => a.launcher_type === type && a.launcher_id === lid, + ); + }; + + const toggleAdmin = async (session: SessionInfo) => { + const type = parseSessionType(session.session_id); + if (!type) return; + const lid = + session.user_id ?? + session.session_id.replace( + /^.*?[._](?:PERSON|GROUP|person|group)[._]/i, + '', + ); + const key = session.session_id; + setTogglingAdmin(key); + try { + const existing = admins.find( + (a: BotAdmin) => a.launcher_type === type && a.launcher_id === lid, + ); + if (existing) { + await httpClient.deleteBotAdmin(botId, existing.id); + toast.success(t('bots.admins.deleteSuccess')); + } else { + await httpClient.addBotAdmin(botId, type, lid); + toast.success(t('bots.admins.addSuccess')); + } + await reloadAdmins(); + } catch { + toast.error(t('bots.admins.addError')); + } finally { + setTogglingAdmin(null); + } + }; + const abbreviateId = (id: string): string => { if (id.length <= 10) return id; return `${id.slice(0, 4)}..${id.slice(-4)}`; @@ -384,257 +436,307 @@ const BotSessionMonitor = forwardRef< ); return ( -
- {/* Left Panel: Session List */} -
- {/* Session List */} - - {loadingSessions && sessions.length === 0 ? ( -
- {t('bots.sessionMonitor.loading')} -
- ) : sessions.length === 0 ? ( -
- {t('bots.sessionMonitor.noSessions')} + <> +
+ {/* Left Panel: Session List */} +
+ {/* Admin header */} +
+ +
+ {/* Session List */} + + {loadingSessions && sessions.length === 0 ? ( +
+ {t('bots.sessionMonitor.loading')} +
+ ) : sessions.length === 0 ? ( +
+ {t('bots.sessionMonitor.noSessions')} +
+ ) : ( +
+ {sessions.map((session) => { + const isSelected = selectedSessionId === session.session_id; + const sessionType = parseSessionType(session.session_id); + const sessionIsAdmin = isSessionAdmin(session); + return ( +
setSelectedSessionId(session.session_id)} + > +
+ + {session.user_name || + session.user_id || + session.session_id.slice(0, 12)} + + + {formatRelativeTime(session.last_activity)} + +
+
+ {sessionType && ( + + {sessionType} + + )} + + {session.user_id && ( + + {abbreviateId(session.user_id)} + + )} + {session.is_active && ( + + + + )} +
+
+ ); + })} +
+ )} +
+
+ + {/* Right Panel: Messages */} +
+ {!selectedSessionId ? ( +
+ {t('bots.sessionMonitor.selectSession')}
) : ( -
- {sessions.map((session) => { - const isSelected = selectedSessionId === session.session_id; - return ( - + + )} + {selectedSession?.is_active && ( + <> + · + + Active - )} -
- - ); - })} -
- )} - -
- - {/* Right Panel: Messages */} -
- {!selectedSessionId ? ( -
- {t('bots.sessionMonitor.selectSession')} -
- ) : ( - <> - {/* Chat Header */} -
-
-
- {selectedSession?.user_name || - selectedSession?.user_id || - selectedSessionId.slice(0, 20)} -
-
- {parseSessionType(selectedSessionId) && ( - {parseSessionType(selectedSessionId)} - )} - {selectedSession?.platform && ( - <> - {parseSessionType(selectedSessionId) && ·} - {selectedSession.platform} - - )} - {selectedSession?.user_id && ( - <> - · - - {selectedSession.user_id} - - - - )} - {selectedSession?.is_active && ( - <> - · - - - Active - - - )} + + )} + {selectedSession && parseSessionType(selectedSessionId) && ( + <> + · + + + )} +
-
- {/* Messages Area */} - -
- {loadingMessages ? ( -
- {t('bots.sessionMonitor.loading')} -
- ) : messages.length === 0 ? ( -
- {t('bots.sessionMonitor.noMessages')} -
- ) : ( - messages.map((msg, msgIndex) => { - const isUser = isUserMessage(msg); - const isDiscarded = - msg.status === 'discarded' || - msg.pipeline_id === PIPELINE_DISCARD; - // For bot replies, find feedback linked to the preceding user message - let msgFeedback: SessionFeedback | undefined; - if (!isUser) { - for (let i = msgIndex - 1; i >= 0; i--) { - if (isUserMessage(messages[i])) { - msgFeedback = feedbackMap[messages[i].id]; - break; + {/* Messages Area */} + +
+ {loadingMessages ? ( +
+ {t('bots.sessionMonitor.loading')} +
+ ) : messages.length === 0 ? ( +
+ {t('bots.sessionMonitor.noMessages')} +
+ ) : ( + messages.map((msg, msgIndex) => { + const isUser = isUserMessage(msg); + const isDiscarded = + msg.status === 'discarded' || + msg.pipeline_id === PIPELINE_DISCARD; + // For bot replies, find feedback linked to the preceding user message + let msgFeedback: SessionFeedback | undefined; + if (!isUser) { + for (let i = msgIndex - 1; i >= 0; i--) { + if (isUserMessage(messages[i])) { + msgFeedback = feedbackMap[messages[i].id]; + break; + } } } - } - return ( -
+ return (
- {renderMessageContent(msg)} - {/* Role label + pipeline + timestamp */}
- - {isUser - ? t('bots.sessionMonitor.userMessage', { - defaultValue: 'User', - }) - : t('bots.sessionMonitor.botMessage', { - defaultValue: 'Assistant', + {renderMessageContent(msg)} + {/* Role label + pipeline + timestamp */} +
+ + {isUser + ? t('bots.sessionMonitor.userMessage', { + defaultValue: 'User', + }) + : t('bots.sessionMonitor.botMessage', { + defaultValue: 'Assistant', + })} + + + {formatTime(msg.timestamp)} + + {isDiscarded ? ( + + + {t('bots.sessionMonitor.discarded', { + defaultValue: 'Discarded', })} - - - {formatTime(msg.timestamp)} - - {isDiscarded ? ( - - - {t('bots.sessionMonitor.discarded', { - defaultValue: 'Discarded', - })} - - ) : msg.pipeline_name ? ( - - - {msg.pipeline_name} - - ) : null} - {msg.status === 'error' && ( - error - )} - {msg.runner_name && ( - - - {msg.runner_name} - - )} - {/* Feedback indicator — same line, pushed right */} - {!isUser && - msgFeedback && - (msgFeedback.feedback_type === 1 ? ( - - - {t('monitoring.feedback.like')} - {msgFeedback.feedback_content && ( - - {msgFeedback.feedback_content} - - )} - ) : ( - - - {t('monitoring.feedback.dislike')} - {msgFeedback.feedback_content && ( - - {msgFeedback.feedback_content} - - )} + ) : msg.pipeline_name ? ( + + + {msg.pipeline_name} - ))} + ) : null} + {msg.status === 'error' && ( + error + )} + {msg.runner_name && ( + + + {msg.runner_name} + + )} + {/* Feedback indicator — same line, pushed right */} + {!isUser && + msgFeedback && + (msgFeedback.feedback_type === 1 ? ( + + + {t('monitoring.feedback.like')} + {msgFeedback.feedback_content && ( + + {msgFeedback.feedback_content} + + )} + + ) : ( + + + {t('monitoring.feedback.dislike')} + {msgFeedback.feedback_content && ( + + {msgFeedback.feedback_content} + + )} + + ))} +
-
- ); - }) - )} -
-
- - )} + ); + }) + )} +
+
+ + )} +
-
+ + + ); }); diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index bfc351f3e..44ad3769b 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -459,6 +459,10 @@ const enUS = { deleteSuccess: 'Admin removed', deleteError: 'Failed to remove admin: ', noAdmins: 'No admins configured', + setAdminTitle: 'Set as admin', + adminBadge: 'Admin', + configureAdmins: 'Manage Admins', + removeAdminTitle: 'Remove admin', }, }, plugins: { diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index dd32f0dae..9a825ad32 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -440,6 +440,10 @@ const zhHans = { deleteSuccess: '已移除', deleteError: '移除失败:', noAdmins: '暂无管理员', + setAdminTitle: '设为管理员', + adminBadge: '管理员', + configureAdmins: '配置管理员', + removeAdminTitle: '移除管理员权限', }, }, plugins: {