Merge remote-tracking branch 'origin/master' into feat/workflow

# Conflicts:
#	pyproject.toml
#	src/langbot/pkg/api/http/controller/main.py
#	src/langbot/pkg/core/app.py
#	src/langbot/pkg/core/stages/build_app.py
#	src/langbot/pkg/provider/modelmgr/requester.py
#	uv.lock
#	web/package-lock.json
#	web/package.json
#	web/pnpm-lock.yaml
#	web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx
#	web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx
#	web/src/app/home/components/home-sidebar/HomeSidebar.tsx
#	web/src/app/infra/entities/api/index.ts
#	web/src/app/infra/http/BackendClient.ts
#	web/src/i18n/locales/en-US.ts
#	web/src/i18n/locales/es-ES.ts
#	web/src/i18n/locales/ja-JP.ts
#	web/src/i18n/locales/ru-RU.ts
#	web/src/i18n/locales/th-TH.ts
#	web/src/i18n/locales/vi-VN.ts
#	web/src/i18n/locales/zh-Hans.ts
#	web/src/i18n/locales/zh-Hant.ts
#	web/src/router.tsx
This commit is contained in:
RockChinQ
2026-07-01 20:33:05 +08:00
773 changed files with 84040 additions and 16377 deletions
@@ -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 };
}
@@ -4,6 +4,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { Switch } from '@/components/ui/switch';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { MessageSquare, Workflow } from 'lucide-react';
export default function BotCard({
botCardVO,
@@ -42,28 +43,14 @@ export default function BotCard({
</div>
<div className={`${styles.basicInfoAdapterContainer}`}>
<svg
className={`${styles.basicInfoAdapterIcon}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M2 8.99374C2 5.68349 4.67654 3 8.00066 3H15.9993C19.3134 3 22 5.69478 22 8.99374V21H8.00066C4.68659 21 2 18.3052 2 15.0063V8.99374ZM20 19V8.99374C20 6.79539 18.2049 5 15.9993 5H8.00066C5.78458 5 4 6.78458 4 8.99374V15.0063C4 17.2046 5.79512 19 8.00066 19H20ZM14 11H16V13H14V11ZM8 11H10V13H8V11Z"></path>
</svg>
<MessageSquare className={`${styles.basicInfoAdapterIcon}`} />
<span className={`${styles.basicInfoAdapterLabel}`}>
{botCardVO.adapterLabel}
</span>
</div>
<div className={`${styles.basicInfoPipelineContainer}`}>
<svg
className={`${styles.basicInfoPipelineIcon}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M6 21.5C4.067 21.5 2.5 19.933 2.5 18C2.5 16.067 4.067 14.5 6 14.5C7.5852 14.5 8.92427 15.5539 9.35481 16.9992L15 16.9994V15L17 14.9994V9.24339L14.757 6.99938H9V9.00003H3V3.00003H9V4.99939H14.757L18 1.75739L22.2426 6.00003L19 9.24139V14.9994L21 15V21H15V18.9994L9.35499 19.0003C8.92464 20.4459 7.58543 21.5 6 21.5ZM6 16.5C5.17157 16.5 4.5 17.1716 4.5 18C4.5 18.8285 5.17157 19.5 6 19.5C6.82843 19.5 7.5 18.8285 7.5 18C7.5 17.1716 6.82843 16.5 6 16.5ZM19 17H17V19H19V17ZM18 4.58581L16.5858 6.00003L18 7.41424L19.4142 6.00003L18 4.58581ZM7 5.00003H5V7.00003H7V5.00003Z"></path>
</svg>
<Workflow className={`${styles.basicInfoPipelineIcon}`} />
<span className={`${styles.basicInfoPipelineLabel}`}>
{botCardVO.usePipelineName}
</span>
@@ -13,6 +13,7 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient';
import { systemInfo } from '@/app/infra/http';
import { Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import { ExternalLink } from 'lucide-react';
@@ -580,6 +581,7 @@ export default function BotForm({
extra_webhook_url: extraWebhookUrl,
bot_uuid: initBotId || '',
adapter_config: form.getValues('adapter_config') || {},
outbound_ips: systemInfo.outbound_ips,
}}
/>
)}
@@ -48,7 +48,6 @@ interface PipelineOption {
}
interface RoutingRulesEditorProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: UseFormReturn<any>;
pipelineNameList: PipelineOption[];
}
@@ -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<string, SessionFeedback>
>({});
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 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 (
<div className="flex flex-col md:flex-row h-full min-h-0 rounded-lg border overflow-hidden">
{/* Left Panel: 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">
{/* 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 className="flex flex-col md:flex-row h-full min-h-0 rounded-lg border overflow-hidden">
{/* Left Panel: 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">
{/* Admin header */}
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between">
<button
type="button"
className="inline-flex items-center gap-1.5 text-sm font-medium hover:text-foreground transition-colors"
onClick={() => setAdminsDialogOpen(true)}
>
<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 className="p-1.5">
{sessions.map((session) => {
const isSelected = selectedSessionId === session.session_id;
return (
<button
key={session.session_id}
type="button"
className={cn(
'w-full text-left px-2.5 py-2 rounded-md transition-colors',
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
<>
{/* Chat Header */}
<div className="px-4 py-2.5 border-b shrink-0 flex items-center justify-between">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">
{selectedSession?.user_name ||
selectedSession?.user_id ||
selectedSessionId.slice(0, 20)}
</div>
<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)}
>
<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">
{parseSessionType(session.session_id) && (
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
{parseSessionType(session.session_id)}
{selectedSession?.user_id && (
<>
<span>·</span>
<span className="font-mono">
{selectedSession.user_id}
</span>
)}
{session.platform && (
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
{session.platform}
</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">
<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>
</button>
);
})}
</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>
) : (
<>
{/* Chat Header */}
<div className="px-4 py-2.5 border-b shrink-0">
<div className="min-w-0">
<div className="text-sm font-medium truncate">
{selectedSession?.user_name ||
selectedSession?.user_id ||
selectedSessionId.slice(0, 20)}
</div>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
{parseSessionType(selectedSessionId) && (
<span>{parseSessionType(selectedSessionId)}</span>
)}
{selectedSession?.platform && (
<>
{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>
</>
)}
</>
)}
{selectedSession && parseSessionType(selectedSessionId) && (
<>
<span>·</span>
<button
type="button"
className={cn(
'inline-flex items-center gap-1 transition-colors',
isSessionAdmin(selectedSession)
? 'text-blue-500'
: 'text-muted-foreground hover:text-blue-500',
)}
disabled={togglingAdmin === selectedSessionId}
title={
isSessionAdmin(selectedSession)
? t('bots.admins.removeAdminTitle')
: t('bots.admins.setAdminTitle')
}
onClick={() => toggleAdmin(selectedSession)}
>
{isSessionAdmin(selectedSession) ? (
<ShieldCheck className="size-3.5" />
) : (
<ShieldOff className="size-3.5" />
)}
<span>{t('bots.admins.adminBadge')}</span>
</button>
</>
)}
</div>
</div>
</div>
</div>
{/* Messages Area */}
<ScrollArea
ref={messagesContainerRef}
className="flex-1 px-4 py-4 overflow-y-auto min-h-0"
>
<div className="space-y-4">
{loadingMessages ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.loading')}
</div>
) : messages.length === 0 ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.noMessages')}
</div>
) : (
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 */}
<ScrollArea
ref={messagesContainerRef}
className="flex-1 px-4 py-4 overflow-y-auto min-h-0"
>
<div className="space-y-4">
{loadingMessages ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.loading')}
</div>
) : messages.length === 0 ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.noMessages')}
</div>
) : (
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 (
<div
key={msg.id}
className={cn(
'flex',
isUser ? 'justify-end' : 'justify-start',
)}
>
return (
<div
key={msg.id}
className={cn(
'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',
'flex',
isUser ? 'justify-end' : 'justify-start',
)}
>
{renderMessageContent(msg)}
{/* Role label + pipeline + timestamp */}
<div
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>
{isUser
? t('bots.sessionMonitor.userMessage', {
defaultValue: 'User',
})
: t('bots.sessionMonitor.botMessage', {
defaultValue: 'Assistant',
{renderMessageContent(msg)}
{/* Role label + pipeline + timestamp */}
<div
className={cn(
'text-[11px] mt-1.5 flex items-center gap-1.5 text-muted-foreground',
)}
>
<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 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>
)}
) : 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 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>
</ScrollArea>
</>
)}
);
})
)}
</div>
</ScrollArea>
</>
)}
</div>
</div>
</div>
<BotAdminsDialog
botId={botId}
open={adminsDialogOpen}
onOpenChange={setAdminsDialogOpen}
admins={admins}
onAdminsChange={reloadAdmins}
/>
</>
);
});