mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-13 22:20:58 +00:00
refactor: move admin management into session monitor, fix session_id enum format
- Remove standalone admins tab, replace with BotAdminsDialog in session monitor - Add admin toggle button inline in chat header next to Active status - Add BotAdminsDialog component with useBotAdmins hook - Fix session_id written as LauncherTypes.PERSON_xxx instead of person_xxx (monitoring_helper.py x4, pipelinemgr.py x1 missing .value on launcher_type) - Fix duplicate platform/person label in session list and chat header - Migrate existing malformed session_id records in DB
This commit is contained in:
@@ -32,7 +32,7 @@ class MonitoringHelper:
|
|||||||
"""Record the start of query processing, returns message_id"""
|
"""Record the start of query processing, returns message_id"""
|
||||||
try:
|
try:
|
||||||
# Check if session exists, if not, record session start
|
# 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
|
# Get sender name from message event
|
||||||
sender_name = None
|
sender_name = None
|
||||||
@@ -137,7 +137,7 @@ class MonitoringHelper:
|
|||||||
):
|
):
|
||||||
"""Record bot response message to monitoring"""
|
"""Record bot response message to monitoring"""
|
||||||
try:
|
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
|
# Get sender name from message event
|
||||||
sender_name = None
|
sender_name = None
|
||||||
@@ -202,7 +202,7 @@ class MonitoringHelper:
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Record query processing error, returns message_id"""
|
"""Record query processing error, returns message_id"""
|
||||||
try:
|
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
|
# Get sender name from message event
|
||||||
sender_name = None
|
sender_name = None
|
||||||
@@ -268,7 +268,7 @@ class MonitoringHelper:
|
|||||||
):
|
):
|
||||||
"""Record LLM call"""
|
"""Record LLM call"""
|
||||||
try:
|
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(
|
await ap.monitoring_service.record_llm_call(
|
||||||
bot_id=bot_id,
|
bot_id=bot_id,
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class RuntimePipeline:
|
|||||||
bot_name = query.variables.get('_monitoring_bot_name', 'Unknown')
|
bot_name = query.variables.get('_monitoring_bot_name', 'Unknown')
|
||||||
pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown')
|
pipeline_name = query.variables.get('_monitoring_pipeline_name', 'Unknown')
|
||||||
message_id = query.variables.get('_monitoring_message_id', '')
|
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
|
# Update message status to error
|
||||||
if message_id:
|
if message_id:
|
||||||
|
|||||||
@@ -22,19 +22,11 @@ import {
|
|||||||
import BotForm from '@/app/home/bots/components/bot-form/BotForm';
|
import BotForm from '@/app/home/bots/components/bot-form/BotForm';
|
||||||
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
|
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
|
||||||
import BotSessionMonitor from '@/app/home/bots/components/bot-session/BotSessionMonitor';
|
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 type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-session/BotSessionMonitor';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { Settings, FileText, Users, RefreshCw, Trash2 } from 'lucide-react';
|
||||||
Settings,
|
|
||||||
FileText,
|
|
||||||
Users,
|
|
||||||
RefreshCw,
|
|
||||||
Trash2,
|
|
||||||
ShieldCheck,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -237,10 +229,6 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="admins" className="gap-1.5">
|
|
||||||
<ShieldCheck className="size-3.5" />
|
|
||||||
{t('bots.admins.title')}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Tab: Configuration */}
|
{/* Tab: Configuration */}
|
||||||
@@ -303,16 +291,6 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
<TabsContent value="sessions" className="flex-1 min-h-0 mt-4">
|
<TabsContent value="sessions" className="flex-1 min-h-0 mt-4">
|
||||||
<BotSessionMonitor ref={sessionMonitorRef} botId={id} />
|
<BotSessionMonitor ref={sessionMonitorRef} botId={id} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Tab: Admins */}
|
|
||||||
<TabsContent
|
|
||||||
value="admins"
|
|
||||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
|
||||||
>
|
|
||||||
<div className="mx-auto max-w-3xl pb-8">
|
|
||||||
<BotAdminsPanel botId={id} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<ShieldCheck className="size-4" />
|
||||||
|
{t('bots.admins.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>{t('bots.admins.description')}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Add row */}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Select value={newType} onValueChange={setNewType}>
|
||||||
|
<SelectTrigger className="w-28 shrink-0">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="person">
|
||||||
|
{t('bots.admins.typePerson')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="group">
|
||||||
|
{t('bots.admins.typeGroup')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
className="flex-1"
|
||||||
|
placeholder={t('bots.admins.placeholderId')}
|
||||||
|
value={newId}
|
||||||
|
onChange={(e) => setNewId(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={adding || !newId.trim()}
|
||||||
|
>
|
||||||
|
<Plus className="size-4 mr-1" />
|
||||||
|
{t('bots.admins.addAdmin')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
{admins.length === 0 ? (
|
||||||
|
<div className="text-sm text-muted-foreground py-6 text-center">
|
||||||
|
{t('bots.admins.noAdmins')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="max-h-64">
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-muted/40">
|
||||||
|
<th className="text-left px-3 py-2 font-medium text-muted-foreground w-28">
|
||||||
|
{t('bots.admins.launcherType')}
|
||||||
|
</th>
|
||||||
|
<th className="text-left px-3 py-2 font-medium text-muted-foreground">
|
||||||
|
{t('bots.admins.launcherId')}
|
||||||
|
</th>
|
||||||
|
<th className="w-10" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{admins.map((admin) => (
|
||||||
|
<tr
|
||||||
|
key={admin.id}
|
||||||
|
className="border-b last:border-0 hover:bg-muted/30"
|
||||||
|
>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<span className="px-1.5 py-0.5 rounded bg-muted text-xs">
|
||||||
|
{admin.launcher_type === 'person'
|
||||||
|
? t('bots.admins.typePerson')
|
||||||
|
: t('bots.admins.typeGroup')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 font-mono">
|
||||||
|
{admin.launcher_id}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-muted-foreground hover:text-destructive transition-colors"
|
||||||
|
onClick={() => handleDelete(admin.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared hook so the session monitor and the dialog stay in sync.
|
||||||
|
export function useBotAdmins(botId: string) {
|
||||||
|
const [admins, setAdmins] = useState<BotAdmin[]>([]);
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -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<BotAdmin[]>([]);
|
|
||||||
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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<Select value={newType} onValueChange={setNewType}>
|
|
||||||
<SelectTrigger className="w-28 shrink-0">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="person">
|
|
||||||
{t('bots.admins.typePerson')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="group">{t('bots.admins.typeGroup')}</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Input
|
|
||||||
className="flex-1"
|
|
||||||
placeholder={t('bots.admins.placeholderId')}
|
|
||||||
value={newId}
|
|
||||||
onChange={(e) => setNewId(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAdd}
|
|
||||||
disabled={adding || !newId.trim()}
|
|
||||||
>
|
|
||||||
<Plus className="size-4 mr-1" />
|
|
||||||
{t('bots.admins.addAdmin')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
||||||
{t('bots.sessionMonitor.loading')}
|
|
||||||
</div>
|
|
||||||
) : admins.length === 0 ? (
|
|
||||||
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
||||||
{t('bots.admins.noAdmins')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="border rounded-md overflow-hidden">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b bg-muted/40">
|
|
||||||
<th className="text-left px-3 py-2 font-medium text-muted-foreground w-28">
|
|
||||||
{t('bots.admins.launcherType')}
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-3 py-2 font-medium text-muted-foreground">
|
|
||||||
{t('bots.admins.launcherId')}
|
|
||||||
</th>
|
|
||||||
<th className="w-10" />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{admins.map((admin) => (
|
|
||||||
<tr
|
|
||||||
key={admin.id}
|
|
||||||
className="border-b last:border-0 hover:bg-muted/30"
|
|
||||||
>
|
|
||||||
<td className="px-3 py-2">
|
|
||||||
<span className="px-1.5 py-0.5 rounded bg-muted text-xs">
|
|
||||||
{admin.launcher_type === 'person'
|
|
||||||
? t('bots.admins.typePerson')
|
|
||||||
: t('bots.admins.typeGroup')}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2 font-mono">{admin.launcher_id}</td>
|
|
||||||
<td className="px-2 py-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-muted-foreground hover:text-destructive transition-colors"
|
|
||||||
onClick={() => handleDelete(admin.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,14 @@ import {
|
|||||||
Workflow,
|
Workflow,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
ThumbsDown,
|
ThumbsDown,
|
||||||
|
ShieldCheck,
|
||||||
|
ShieldOff,
|
||||||
} from 'lucide-react';
|
} 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 { copyToClipboard } from '@/app/utils/clipboard';
|
||||||
import {
|
import {
|
||||||
MessageChainComponent,
|
MessageChainComponent,
|
||||||
@@ -94,15 +101,60 @@ const BotSessionMonitor = forwardRef<
|
|||||||
Record<string, SessionFeedback>
|
Record<string, SessionFeedback>
|
||||||
>({});
|
>({});
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { admins, reload: reloadAdmins } = useBotAdmins(botId);
|
||||||
|
const [adminsDialogOpen, setAdminsDialogOpen] = useState(false);
|
||||||
|
const [togglingAdmin, setTogglingAdmin] = useState<string | null>(null);
|
||||||
|
|
||||||
const parseSessionType = (sessionId: string): string | null => {
|
const parseSessionType = (sessionId: string): string | null => {
|
||||||
const idx = sessionId.indexOf('_');
|
const lower = sessionId.toLowerCase();
|
||||||
if (idx === -1) return null;
|
if (lower.includes('person')) return 'person';
|
||||||
const type = sessionId.slice(0, idx);
|
if (lower.includes('group')) return 'group';
|
||||||
if (type === 'person' || type === 'group') return type;
|
|
||||||
return null;
|
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 => {
|
const abbreviateId = (id: string): string => {
|
||||||
if (id.length <= 10) return id;
|
if (id.length <= 10) return id;
|
||||||
return `${id.slice(0, 4)}..${id.slice(-4)}`;
|
return `${id.slice(0, 4)}..${id.slice(-4)}`;
|
||||||
@@ -384,257 +436,307 @@ const BotSessionMonitor = forwardRef<
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row h-full min-h-0 rounded-lg border overflow-hidden">
|
<>
|
||||||
{/* Left Panel: Session List */}
|
<div className="flex flex-col md:flex-row h-full min-h-0 rounded-lg border overflow-hidden">
|
||||||
<div className="max-h-48 md:max-h-none md:w-60 flex-shrink-0 border-b md:border-b-0 md:border-r flex flex-col min-h-0">
|
{/* Left Panel: Session List */}
|
||||||
{/* Session List */}
|
<div className="max-h-48 md:max-h-none md:w-60 flex-shrink-0 border-b md:border-b-0 md:border-r flex flex-col min-h-0">
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
{/* Admin header */}
|
||||||
{loadingSessions && sessions.length === 0 ? (
|
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between">
|
||||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
<button
|
||||||
{t('bots.sessionMonitor.loading')}
|
type="button"
|
||||||
</div>
|
className="inline-flex items-center gap-1.5 text-sm font-medium hover:text-foreground transition-colors"
|
||||||
) : sessions.length === 0 ? (
|
onClick={() => setAdminsDialogOpen(true)}
|
||||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
>
|
||||||
{t('bots.sessionMonitor.noSessions')}
|
<ShieldCheck className="size-4" />
|
||||||
|
<span>
|
||||||
|
{t('bots.admins.configureAdmins')}
|
||||||
|
{admins.length > 0 && (
|
||||||
|
<span className="ml-1 tabular-nums text-xs text-muted-foreground">
|
||||||
|
({admins.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Session List */}
|
||||||
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
|
{loadingSessions && sessions.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||||
|
{t('bots.sessionMonitor.loading')}
|
||||||
|
</div>
|
||||||
|
) : sessions.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||||
|
{t('bots.sessionMonitor.noSessions')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-1.5">
|
||||||
|
{sessions.map((session) => {
|
||||||
|
const isSelected = selectedSessionId === session.session_id;
|
||||||
|
const sessionType = parseSessionType(session.session_id);
|
||||||
|
const sessionIsAdmin = isSessionAdmin(session);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={session.session_id}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-left px-2.5 py-2 rounded-md transition-colors cursor-pointer',
|
||||||
|
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedSessionId(session.session_id)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-0.5">
|
||||||
|
<span className="text-sm font-medium truncate mr-2">
|
||||||
|
{session.user_name ||
|
||||||
|
session.user_id ||
|
||||||
|
session.session_id.slice(0, 12)}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-muted-foreground tabular-nums flex-shrink-0">
|
||||||
|
{formatRelativeTime(session.last_activity)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
{sessionType && (
|
||||||
|
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
|
||||||
|
{sessionType}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{session.user_id && (
|
||||||
|
<span className="truncate text-[10px]">
|
||||||
|
{abbreviateId(session.user_id)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{session.is_active && (
|
||||||
|
<span className="flex items-center gap-0.5 text-green-600 dark:text-green-400">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel: Messages */}
|
||||||
|
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||||
|
{!selectedSessionId ? (
|
||||||
|
<div className="text-center text-muted-foreground text-sm flex-1 flex items-center justify-center">
|
||||||
|
{t('bots.sessionMonitor.selectSession')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-1.5">
|
<>
|
||||||
{sessions.map((session) => {
|
{/* Chat Header */}
|
||||||
const isSelected = selectedSessionId === session.session_id;
|
<div className="px-4 py-2.5 border-b shrink-0 flex items-center justify-between">
|
||||||
return (
|
<div className="min-w-0 flex-1">
|
||||||
<button
|
<div className="text-sm font-medium truncate">
|
||||||
key={session.session_id}
|
{selectedSession?.user_name ||
|
||||||
type="button"
|
selectedSession?.user_id ||
|
||||||
className={cn(
|
selectedSessionId.slice(0, 20)}
|
||||||
'w-full text-left px-2.5 py-2 rounded-md transition-colors',
|
</div>
|
||||||
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
|
||||||
|
{parseSessionType(selectedSessionId) && (
|
||||||
|
<span>{parseSessionType(selectedSessionId)}</span>
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedSessionId(session.session_id)}
|
{selectedSession?.user_id && (
|
||||||
>
|
<>
|
||||||
<div className="flex items-center justify-between mb-0.5">
|
<span>·</span>
|
||||||
<span className="text-sm font-medium truncate mr-2">
|
<span className="font-mono">
|
||||||
{session.user_name ||
|
{selectedSession.user_id}
|
||||||
session.user_id ||
|
|
||||||
session.session_id.slice(0, 12)}
|
|
||||||
</span>
|
|
||||||
<span className="text-[11px] text-muted-foreground tabular-nums flex-shrink-0">
|
|
||||||
{formatRelativeTime(session.last_activity)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
{parseSessionType(session.session_id) && (
|
|
||||||
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
|
|
||||||
{parseSessionType(session.session_id)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<button
|
||||||
{session.platform && (
|
type="button"
|
||||||
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
|
onClick={() => copyUserId(selectedSession.user_id!)}
|
||||||
{session.platform}
|
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||||
</span>
|
title={t('common.copy')}
|
||||||
)}
|
>
|
||||||
{session.user_id && (
|
{copiedUserId ? (
|
||||||
<span className="truncate text-[10px]">
|
<Check className="w-3 h-3 text-green-600" />
|
||||||
{abbreviateId(session.user_id)}
|
) : (
|
||||||
</span>
|
<Copy className="w-3 h-3" />
|
||||||
)}
|
)}
|
||||||
{session.is_active && (
|
</button>
|
||||||
<span className="flex items-center gap-0.5 text-green-600 dark:text-green-400">
|
</>
|
||||||
|
)}
|
||||||
|
{selectedSession?.is_active && (
|
||||||
|
<>
|
||||||
|
<span>·</span>
|
||||||
|
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
||||||
|
Active
|
||||||
</span>
|
</span>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</button>
|
{selectedSession && parseSessionType(selectedSessionId) && (
|
||||||
);
|
<>
|
||||||
})}
|
<span>·</span>
|
||||||
</div>
|
<button
|
||||||
)}
|
type="button"
|
||||||
</ScrollArea>
|
className={cn(
|
||||||
</div>
|
'inline-flex items-center gap-1 transition-colors',
|
||||||
|
isSessionAdmin(selectedSession)
|
||||||
{/* Right Panel: Messages */}
|
? 'text-blue-500'
|
||||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
: 'text-muted-foreground hover:text-blue-500',
|
||||||
{!selectedSessionId ? (
|
)}
|
||||||
<div className="text-center text-muted-foreground text-sm flex-1 flex items-center justify-center">
|
disabled={togglingAdmin === selectedSessionId}
|
||||||
{t('bots.sessionMonitor.selectSession')}
|
title={
|
||||||
</div>
|
isSessionAdmin(selectedSession)
|
||||||
) : (
|
? t('bots.admins.removeAdminTitle')
|
||||||
<>
|
: t('bots.admins.setAdminTitle')
|
||||||
{/* Chat Header */}
|
}
|
||||||
<div className="px-4 py-2.5 border-b shrink-0">
|
onClick={() => toggleAdmin(selectedSession)}
|
||||||
<div className="min-w-0">
|
>
|
||||||
<div className="text-sm font-medium truncate">
|
{isSessionAdmin(selectedSession) ? (
|
||||||
{selectedSession?.user_name ||
|
<ShieldCheck className="size-3.5" />
|
||||||
selectedSession?.user_id ||
|
) : (
|
||||||
selectedSessionId.slice(0, 20)}
|
<ShieldOff className="size-3.5" />
|
||||||
</div>
|
)}
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
|
<span>{t('bots.admins.adminBadge')}</span>
|
||||||
{parseSessionType(selectedSessionId) && (
|
</button>
|
||||||
<span>{parseSessionType(selectedSessionId)}</span>
|
</>
|
||||||
)}
|
)}
|
||||||
{selectedSession?.platform && (
|
</div>
|
||||||
<>
|
|
||||||
{parseSessionType(selectedSessionId) && <span>·</span>}
|
|
||||||
<span>{selectedSession.platform}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedSession?.user_id && (
|
|
||||||
<>
|
|
||||||
<span>·</span>
|
|
||||||
<span className="font-mono">
|
|
||||||
{selectedSession.user_id}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyUserId(selectedSession.user_id!)}
|
|
||||||
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
title={t('common.copy')}
|
|
||||||
>
|
|
||||||
{copiedUserId ? (
|
|
||||||
<Check className="w-3 h-3 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedSession?.is_active && (
|
|
||||||
<>
|
|
||||||
<span>·</span>
|
|
||||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages Area */}
|
{/* Messages Area */}
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
ref={messagesContainerRef}
|
ref={messagesContainerRef}
|
||||||
className="flex-1 px-4 py-4 overflow-y-auto min-h-0"
|
className="flex-1 px-4 py-4 overflow-y-auto min-h-0"
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{loadingMessages ? (
|
{loadingMessages ? (
|
||||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||||
{t('bots.sessionMonitor.loading')}
|
{t('bots.sessionMonitor.loading')}
|
||||||
</div>
|
</div>
|
||||||
) : messages.length === 0 ? (
|
) : messages.length === 0 ? (
|
||||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||||
{t('bots.sessionMonitor.noMessages')}
|
{t('bots.sessionMonitor.noMessages')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
messages.map((msg, msgIndex) => {
|
messages.map((msg, msgIndex) => {
|
||||||
const isUser = isUserMessage(msg);
|
const isUser = isUserMessage(msg);
|
||||||
const isDiscarded =
|
const isDiscarded =
|
||||||
msg.status === 'discarded' ||
|
msg.status === 'discarded' ||
|
||||||
msg.pipeline_id === PIPELINE_DISCARD;
|
msg.pipeline_id === PIPELINE_DISCARD;
|
||||||
// For bot replies, find feedback linked to the preceding user message
|
// For bot replies, find feedback linked to the preceding user message
|
||||||
let msgFeedback: SessionFeedback | undefined;
|
let msgFeedback: SessionFeedback | undefined;
|
||||||
if (!isUser) {
|
if (!isUser) {
|
||||||
for (let i = msgIndex - 1; i >= 0; i--) {
|
for (let i = msgIndex - 1; i >= 0; i--) {
|
||||||
if (isUserMessage(messages[i])) {
|
if (isUserMessage(messages[i])) {
|
||||||
msgFeedback = feedbackMap[messages[i].id];
|
msgFeedback = feedbackMap[messages[i].id];
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return (
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={msg.id}
|
|
||||||
className={cn(
|
|
||||||
'flex',
|
|
||||||
isUser ? 'justify-end' : 'justify-start',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
|
key={msg.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-3xl px-4 py-2.5 rounded-2xl text-sm',
|
'flex',
|
||||||
isUser
|
isUser ? 'justify-end' : 'justify-start',
|
||||||
? 'bg-primary/10 rounded-br-sm'
|
|
||||||
: 'bg-muted rounded-bl-sm',
|
|
||||||
msg.status === 'error' && 'ring-1 ring-red-400/50',
|
|
||||||
isDiscarded && 'opacity-60',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderMessageContent(msg)}
|
|
||||||
{/* Role label + pipeline + timestamp */}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] mt-1.5 flex items-center gap-1.5 text-muted-foreground',
|
'max-w-3xl px-4 py-2.5 rounded-2xl text-sm',
|
||||||
|
isUser
|
||||||
|
? 'bg-primary/10 rounded-br-sm'
|
||||||
|
: 'bg-muted rounded-bl-sm',
|
||||||
|
msg.status === 'error' &&
|
||||||
|
'ring-1 ring-red-400/50',
|
||||||
|
isDiscarded && 'opacity-60',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>
|
{renderMessageContent(msg)}
|
||||||
{isUser
|
{/* Role label + pipeline + timestamp */}
|
||||||
? t('bots.sessionMonitor.userMessage', {
|
<div
|
||||||
defaultValue: 'User',
|
className={cn(
|
||||||
})
|
'text-[11px] mt-1.5 flex items-center gap-1.5 text-muted-foreground',
|
||||||
: t('bots.sessionMonitor.botMessage', {
|
)}
|
||||||
defaultValue: 'Assistant',
|
>
|
||||||
|
<span>
|
||||||
|
{isUser
|
||||||
|
? t('bots.sessionMonitor.userMessage', {
|
||||||
|
defaultValue: 'User',
|
||||||
|
})
|
||||||
|
: t('bots.sessionMonitor.botMessage', {
|
||||||
|
defaultValue: 'Assistant',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="tabular-nums">
|
||||||
|
{formatTime(msg.timestamp)}
|
||||||
|
</span>
|
||||||
|
{isDiscarded ? (
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-destructive">
|
||||||
|
<Ban className="w-3 h-3" />
|
||||||
|
{t('bots.sessionMonitor.discarded', {
|
||||||
|
defaultValue: 'Discarded',
|
||||||
})}
|
})}
|
||||||
</span>
|
|
||||||
<span className="tabular-nums">
|
|
||||||
{formatTime(msg.timestamp)}
|
|
||||||
</span>
|
|
||||||
{isDiscarded ? (
|
|
||||||
<span className="inline-flex items-center gap-0.5 text-destructive">
|
|
||||||
<Ban className="w-3 h-3" />
|
|
||||||
{t('bots.sessionMonitor.discarded', {
|
|
||||||
defaultValue: 'Discarded',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
) : msg.pipeline_name ? (
|
|
||||||
<span className="inline-flex items-center gap-0.5 opacity-70">
|
|
||||||
<Workflow className="w-3 h-3" />
|
|
||||||
{msg.pipeline_name}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{msg.status === 'error' && (
|
|
||||||
<span className="text-red-500">error</span>
|
|
||||||
)}
|
|
||||||
{msg.runner_name && (
|
|
||||||
<span className="inline-flex items-center gap-0.5 opacity-70">
|
|
||||||
<Bot className="w-3 h-3" />
|
|
||||||
{msg.runner_name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{/* Feedback indicator — same line, pushed right */}
|
|
||||||
{!isUser &&
|
|
||||||
msgFeedback &&
|
|
||||||
(msgFeedback.feedback_type === 1 ? (
|
|
||||||
<span className="inline-flex items-center gap-1 ml-auto text-green-600 dark:text-green-400 cursor-default relative group">
|
|
||||||
<ThumbsUp className="w-3 h-3 flex-shrink-0" />
|
|
||||||
{t('monitoring.feedback.like')}
|
|
||||||
{msgFeedback.feedback_content && (
|
|
||||||
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
|
||||||
{msgFeedback.feedback_content}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : msg.pipeline_name ? (
|
||||||
<span className="inline-flex items-center gap-1 ml-auto text-red-500 dark:text-red-400 cursor-default relative group">
|
<span className="inline-flex items-center gap-0.5 opacity-70">
|
||||||
<ThumbsDown className="w-3 h-3 flex-shrink-0" />
|
<Workflow className="w-3 h-3" />
|
||||||
{t('monitoring.feedback.dislike')}
|
{msg.pipeline_name}
|
||||||
{msgFeedback.feedback_content && (
|
|
||||||
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
|
||||||
{msgFeedback.feedback_content}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
) : null}
|
||||||
|
{msg.status === 'error' && (
|
||||||
|
<span className="text-red-500">error</span>
|
||||||
|
)}
|
||||||
|
{msg.runner_name && (
|
||||||
|
<span className="inline-flex items-center gap-0.5 opacity-70">
|
||||||
|
<Bot className="w-3 h-3" />
|
||||||
|
{msg.runner_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* Feedback indicator — same line, pushed right */}
|
||||||
|
{!isUser &&
|
||||||
|
msgFeedback &&
|
||||||
|
(msgFeedback.feedback_type === 1 ? (
|
||||||
|
<span className="inline-flex items-center gap-1 ml-auto text-green-600 dark:text-green-400 cursor-default relative group">
|
||||||
|
<ThumbsUp className="w-3 h-3 flex-shrink-0" />
|
||||||
|
{t('monitoring.feedback.like')}
|
||||||
|
{msgFeedback.feedback_content && (
|
||||||
|
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
||||||
|
{msgFeedback.feedback_content}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 ml-auto text-red-500 dark:text-red-400 cursor-default relative group">
|
||||||
|
<ThumbsDown className="w-3 h-3 flex-shrink-0" />
|
||||||
|
{t('monitoring.feedback.dislike')}
|
||||||
|
{msgFeedback.feedback_content && (
|
||||||
|
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
||||||
|
{msgFeedback.feedback_content}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})
|
||||||
})
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</ScrollArea>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<BotAdminsDialog
|
||||||
|
botId={botId}
|
||||||
|
open={adminsDialogOpen}
|
||||||
|
onOpenChange={setAdminsDialogOpen}
|
||||||
|
admins={admins}
|
||||||
|
onAdminsChange={reloadAdmins}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -459,6 +459,10 @@ const enUS = {
|
|||||||
deleteSuccess: 'Admin removed',
|
deleteSuccess: 'Admin removed',
|
||||||
deleteError: 'Failed to remove admin: ',
|
deleteError: 'Failed to remove admin: ',
|
||||||
noAdmins: 'No admins configured',
|
noAdmins: 'No admins configured',
|
||||||
|
setAdminTitle: 'Set as admin',
|
||||||
|
adminBadge: 'Admin',
|
||||||
|
configureAdmins: 'Manage Admins',
|
||||||
|
removeAdminTitle: 'Remove admin',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|||||||
@@ -440,6 +440,10 @@ const zhHans = {
|
|||||||
deleteSuccess: '已移除',
|
deleteSuccess: '已移除',
|
||||||
deleteError: '移除失败:',
|
deleteError: '移除失败:',
|
||||||
noAdmins: '暂无管理员',
|
noAdmins: '暂无管理员',
|
||||||
|
setAdminTitle: '设为管理员',
|
||||||
|
adminBadge: '管理员',
|
||||||
|
configureAdmins: '配置管理员',
|
||||||
|
removeAdminTitle: '移除管理员权限',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|||||||
Reference in New Issue
Block a user