mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 20:27:13 +00:00
refactor(pipelines): streamline debug chat controls
This commit is contained in:
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { DialogContent } from '@/components/ui/dialog';
|
import { DialogContent } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -42,11 +42,6 @@ import {
|
|||||||
AlignLeft,
|
AlignLeft,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip';
|
|
||||||
|
|
||||||
interface DebugDialogProps {
|
interface DebugDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -155,7 +150,7 @@ export default function DebugDialog({
|
|||||||
);
|
);
|
||||||
const [streamOutput, setStreamOutput] = useState(true);
|
const [streamOutput, setStreamOutput] = useState(true);
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const wsClientRef = useRef<WebSocketClient | null>(null);
|
const wsClientRef = useRef<WebSocketClient | null>(null);
|
||||||
@@ -367,7 +362,7 @@ export default function DebugDialog({
|
|||||||
}
|
}
|
||||||
}, [showAtPopover]);
|
}, [showAtPopover]);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (sessionType === 'group') {
|
if (sessionType === 'group') {
|
||||||
if (value.endsWith('@')) {
|
if (value.endsWith('@')) {
|
||||||
@@ -853,75 +848,50 @@ export default function DebugDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderContent = () => (
|
const renderContent = () => (
|
||||||
<div className="flex flex-1 h-full min-h-0">
|
<div className="flex flex-1 h-full min-h-0 flex-col">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-14 p-2 pl-0 shrink-0 flex flex-col justify-start gap-2',
|
'flex shrink-0 flex-wrap items-center gap-1 border-b px-4 py-2',
|
||||||
compact && 'w-12 p-1.5 pl-1',
|
compact && 'px-3',
|
||||||
)}
|
)}
|
||||||
|
data-debug-session-toolbar="true"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<span className="mr-1 text-xs text-muted-foreground">
|
||||||
<TooltipTrigger asChild>
|
{t('pipelines.debugDialog.sessionType')}
|
||||||
<Button
|
</span>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
type="button"
|
||||||
aria-label={t('pipelines.debugDialog.privateChat')}
|
variant={sessionType === 'person' ? 'secondary' : 'ghost'}
|
||||||
className={cn(
|
size="sm"
|
||||||
'w-10 h-10 justify-center rounded-md transition-none border-0 shadow-none',
|
aria-pressed={sessionType === 'person'}
|
||||||
sessionType === 'person'
|
onClick={() => setSessionType('person')}
|
||||||
? 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
>
|
||||||
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
<User className="size-4" />
|
||||||
)}
|
{t('pipelines.debugDialog.privateChat')}
|
||||||
onClick={() => setSessionType('person')}
|
</Button>
|
||||||
>
|
<Button
|
||||||
<User className="size-5" />
|
type="button"
|
||||||
</Button>
|
variant={sessionType === 'group' ? 'secondary' : 'ghost'}
|
||||||
</TooltipTrigger>
|
size="sm"
|
||||||
<TooltipContent side="right">
|
aria-pressed={sessionType === 'group'}
|
||||||
{t('pipelines.debugDialog.privateChat')}
|
onClick={() => setSessionType('group')}
|
||||||
</TooltipContent>
|
>
|
||||||
</Tooltip>
|
<Users className="size-4" />
|
||||||
<Tooltip>
|
{t('pipelines.debugDialog.groupChat')}
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
aria-label={t('pipelines.debugDialog.groupChat')}
|
size="sm"
|
||||||
className={cn(
|
className="ml-auto text-muted-foreground"
|
||||||
'w-10 h-10 justify-center rounded-md transition-none border-0 shadow-none',
|
onClick={() => void resetConversation()}
|
||||||
sessionType === 'group'
|
>
|
||||||
? 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
<RotateCcw className="size-4" />
|
||||||
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
{t('pipelines.debugDialog.reset')}
|
||||||
)}
|
</Button>
|
||||||
onClick={() => setSessionType('group')}
|
|
||||||
>
|
|
||||||
<Users className="size-5" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
{t('pipelines.debugDialog.groupChat')}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
aria-label={t('pipelines.debugDialog.reset')}
|
|
||||||
className="w-10 h-10 justify-center rounded-md text-muted-foreground"
|
|
||||||
onClick={() => void resetConversation()}
|
|
||||||
>
|
|
||||||
<RotateCcw className="size-5" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
{t('pipelines.debugDialog.reset')}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
|
<div className="flex-1 flex flex-col w-full h-full min-h-0">
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1086,9 +1056,10 @@ export default function DebugDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn('p-4 pb-0 flex gap-2', compact && 'flex-col p-3 pb-0')}
|
className={cn('shrink-0 border-t p-4', compact && 'p-3')}
|
||||||
|
data-debug-composer="true"
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t('pipelines.debugDialog.streamOutput')}
|
{t('pipelines.debugDialog.streamOutput')}
|
||||||
@@ -1118,73 +1089,82 @@ export default function DebugDialog({
|
|||||||
<ImageIcon className="size-5" />
|
<ImageIcon className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center gap-2">
|
|
||||||
{hasAt && (
|
<div className="flex min-w-0 items-end gap-2">
|
||||||
<AtBadge targetName="websocketbot" onRemove={handleAtRemove} />
|
<div className="min-w-0 flex-1">
|
||||||
)}
|
{hasAt && (
|
||||||
<div className="relative flex-1">
|
<div className="mb-1">
|
||||||
<Input
|
<AtBadge
|
||||||
ref={inputRef}
|
targetName="websocketbot"
|
||||||
value={inputValue}
|
onRemove={handleAtRemove}
|
||||||
onChange={handleInputChange}
|
/>
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
placeholder={t('pipelines.debugDialog.inputPlaceholder', {
|
|
||||||
type:
|
|
||||||
sessionType === 'person'
|
|
||||||
? t('pipelines.debugDialog.privateChat')
|
|
||||||
: t('pipelines.debugDialog.groupChat'),
|
|
||||||
})}
|
|
||||||
disabled={!isConnected || isUploading}
|
|
||||||
className="flex-1 rounded-md px-3 py-2 transition-none text-base disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
{showAtPopover && (
|
|
||||||
<div
|
|
||||||
ref={popoverRef}
|
|
||||||
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-popover text-popover-foreground shadow-lg"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
|
|
||||||
isHovering ? 'bg-accent' : '',
|
|
||||||
)}
|
|
||||||
onClick={handleAtSelect}
|
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
@websocketbot - {t('pipelines.debugDialog.atTips')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder={t('pipelines.debugDialog.inputPlaceholder', {
|
||||||
|
type:
|
||||||
|
sessionType === 'person'
|
||||||
|
? t('pipelines.debugDialog.privateChat')
|
||||||
|
: t('pipelines.debugDialog.groupChat'),
|
||||||
|
})}
|
||||||
|
disabled={!isConnected || isUploading}
|
||||||
|
rows={1}
|
||||||
|
className="h-11 min-h-11 max-h-32 resize-y rounded-md px-3 py-2 text-sm transition-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
{showAtPopover && (
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-popover text-popover-foreground shadow-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
|
||||||
|
isHovering ? 'bg-accent' : '',
|
||||||
|
)}
|
||||||
|
onClick={handleAtSelect}
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
@websocketbot - {t('pipelines.debugDialog.atTips')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={
|
||||||
|
(!inputValue.trim() &&
|
||||||
|
!hasAt &&
|
||||||
|
selectedImages.length === 0 &&
|
||||||
|
!quotedMessage) ||
|
||||||
|
!isConnected ||
|
||||||
|
isUploading
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
'h-11 shrink-0 rounded-md px-4 text-sm font-medium transition-none shadow-none disabled:opacity-50',
|
||||||
|
!compact && 'px-6 text-base',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
t('pipelines.debugDialog.uploading')
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="size-4" />
|
||||||
|
{hasUnsavedChanges
|
||||||
|
? t('pipelines.debugDialog.saveAndSend')
|
||||||
|
: t('pipelines.debugDialog.send')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={sendMessage}
|
|
||||||
disabled={
|
|
||||||
(!inputValue.trim() &&
|
|
||||||
!hasAt &&
|
|
||||||
selectedImages.length === 0 &&
|
|
||||||
!quotedMessage) ||
|
|
||||||
!isConnected ||
|
|
||||||
isUploading
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
'rounded-md w-20 px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none disabled:opacity-50',
|
|
||||||
compact && 'w-auto px-3 text-sm',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
t('pipelines.debugDialog.uploading')
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Send className="size-4" />
|
|
||||||
{hasUnsavedChanges
|
|
||||||
? t('pipelines.debugDialog.saveAndSend')
|
|
||||||
: t('pipelines.debugDialog.send')}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -254,6 +254,41 @@ test.describe('processor detail workbench', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
debugPanel.getByRole('button', { name: 'Group Chat' }),
|
debugPanel.getByRole('button', { name: 'Group Chat' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
const sessionToolbar = debugPanel.locator(
|
||||||
|
'[data-debug-session-toolbar="true"]',
|
||||||
|
);
|
||||||
|
await expect(sessionToolbar.getByText('Session Type')).toBeVisible();
|
||||||
|
const privateChatButton = sessionToolbar.getByRole('button', {
|
||||||
|
name: 'Private Chat',
|
||||||
|
});
|
||||||
|
const groupChatButton = sessionToolbar.getByRole('button', {
|
||||||
|
name: 'Group Chat',
|
||||||
|
});
|
||||||
|
await expect(privateChatButton).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await groupChatButton.click();
|
||||||
|
await expect(groupChatButton).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await privateChatButton.click();
|
||||||
|
|
||||||
|
const composer = debugPanel.locator('[data-debug-composer="true"]');
|
||||||
|
const messageInput = composer.locator('textarea');
|
||||||
|
const sendButton = composer.getByRole('button', { name: 'Send' });
|
||||||
|
await expect(messageInput).toBeVisible();
|
||||||
|
await expect(messageInput).toHaveAttribute('rows', '1');
|
||||||
|
const inputBox = await messageInput.boundingBox();
|
||||||
|
const sendBox = await sendButton.boundingBox();
|
||||||
|
const toolbarBox = await sessionToolbar.boundingBox();
|
||||||
|
const emptyStateBox = await debugPanel
|
||||||
|
.getByText('No messages', { exact: true })
|
||||||
|
.boundingBox();
|
||||||
|
expect(inputBox).not.toBeNull();
|
||||||
|
expect(sendBox).not.toBeNull();
|
||||||
|
expect(toolbarBox).not.toBeNull();
|
||||||
|
expect(emptyStateBox).not.toBeNull();
|
||||||
|
expect(sendBox!.x).toBeGreaterThan(inputBox!.x + inputBox!.width);
|
||||||
|
expect(Math.abs(sendBox!.height - inputBox!.height)).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(sendBox!.y - inputBox!.y)).toBeLessThanOrEqual(1);
|
||||||
|
expect(toolbarBox!.y).toBeLessThan(emptyStateBox!.y);
|
||||||
|
|
||||||
await debugPanel
|
await debugPanel
|
||||||
.getByRole('button', { name: 'Reset Conversation' })
|
.getByRole('button', { name: 'Reset Conversation' })
|
||||||
.click();
|
.click();
|
||||||
|
|||||||
Reference in New Issue
Block a user