refactor(pipelines): streamline debug chat controls

This commit is contained in:
RockChinQ
2026-08-26 00:18:46 +08:00
parent 532d1b73d2
commit 0c5656d0d0
2 changed files with 151 additions and 136 deletions
@@ -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();