mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1771 from KrishavRajSingh/feat/expand_ui
ui: expand msg
This commit is contained in:
commit
62c73b244d
|
@ -0,0 +1,175 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Expand,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Loader2,
|
||||
Clock,
|
||||
MessageSquareText,
|
||||
Copy,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { ToolViewProps } from '../types';
|
||||
import { formatTimestamp, getToolTitle } from '../utils';
|
||||
import { extractExpandMessageData } from './_utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import Markdown from 'react-markdown';
|
||||
|
||||
export function ExpandMessageToolView({
|
||||
name = 'expand_message',
|
||||
assistantContent,
|
||||
toolContent,
|
||||
assistantTimestamp,
|
||||
toolTimestamp,
|
||||
isSuccess = true,
|
||||
isStreaming = false,
|
||||
}: ToolViewProps) {
|
||||
const {
|
||||
messageId,
|
||||
message,
|
||||
status,
|
||||
actualIsSuccess,
|
||||
actualToolTimestamp,
|
||||
actualAssistantTimestamp
|
||||
} = extractExpandMessageData(
|
||||
assistantContent,
|
||||
toolContent,
|
||||
isSuccess,
|
||||
toolTimestamp,
|
||||
assistantTimestamp
|
||||
);
|
||||
|
||||
const [isCopying, setIsCopying] = React.useState(false);
|
||||
const toolTitle = getToolTitle(name) || 'Message Expansion';
|
||||
|
||||
const copyToClipboard = React.useCallback(async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCopyMessage = React.useCallback(async () => {
|
||||
if (!message) return;
|
||||
|
||||
setIsCopying(true);
|
||||
const success = await copyToClipboard(message);
|
||||
if (success) {
|
||||
toast.success('Message copied to clipboard');
|
||||
} else {
|
||||
toast.error('Failed to copy message');
|
||||
}
|
||||
setTimeout(() => setIsCopying(false), 500);
|
||||
}, [message, copyToClipboard]);
|
||||
|
||||
return (
|
||||
<Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card">
|
||||
<CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative p-2 rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 border border-purple-500/20">
|
||||
<Expand className="w-5 h-5 text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{toolTitle}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isStreaming && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
actualIsSuccess
|
||||
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
|
||||
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
|
||||
}
|
||||
>
|
||||
{actualIsSuccess ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 mr-1" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
{actualIsSuccess ? 'Expanded' : 'Failed'}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{isStreaming && (
|
||||
<Badge className="bg-gradient-to-b from-purple-200 to-purple-100 text-purple-700 dark:from-purple-800/50 dark:to-purple-900/60 dark:text-purple-300">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1" />
|
||||
Expanding
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0 flex-1 overflow-hidden relative">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Message ID */}
|
||||
{messageId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="font-mono text-xs bg-purple-50 dark:bg-purple-950/20 border-purple-200 dark:border-purple-800"
|
||||
>
|
||||
ID: {messageId}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded Message Content - Simple display */}
|
||||
{message ? (
|
||||
<div className="bg-muted/30 rounded-lg p-4 border border-border overflow-hidden">
|
||||
<div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 break-words overflow-wrap-anywhere [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_code]:break-words">
|
||||
<Markdown>{message}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
) : !isStreaming ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center mb-4 border-2 border-purple-200 dark:border-purple-800">
|
||||
<MessageSquareText className="h-8 w-8 text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{actualIsSuccess ? 'No Message Content' : 'Expansion Failed'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
{actualIsSuccess
|
||||
? 'The expanded message does not contain any displayable content.'
|
||||
: 'Unable to expand the requested message. It may not exist or you may not have access to it.'}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
|
||||
<div className="px-4 py-2 h-10 backdrop-blur-sm border-t border-purple-200 dark:border-purple-800 flex justify-between items-center gap-4">
|
||||
<div className="h-full flex items-center gap-2 text-sm text-purple-600 dark:text-purple-400">
|
||||
<Badge className="h-6 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800" variant="outline">
|
||||
<Expand className="h-3 w-3 mr-1" />
|
||||
Message Retrieval
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
{actualToolTimestamp
|
||||
? formatTimestamp(actualToolTimestamp)
|
||||
: actualAssistantTimestamp
|
||||
? formatTimestamp(actualAssistantTimestamp)
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { extractToolData } from '../utils';
|
||||
|
||||
export interface ExpandMessageData {
|
||||
messageId?: string;
|
||||
message?: string;
|
||||
status?: string;
|
||||
actualIsSuccess: boolean;
|
||||
actualToolTimestamp?: string;
|
||||
actualAssistantTimestamp?: string;
|
||||
}
|
||||
|
||||
const parseContent = (content: any): any => {
|
||||
if (typeof content === 'string') {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
const extractFromNewFormat = (content: any): {
|
||||
messageId?: string;
|
||||
message?: string;
|
||||
status?: string;
|
||||
success?: boolean;
|
||||
timestamp?: string;
|
||||
} => {
|
||||
const parsedContent = parseContent(content);
|
||||
|
||||
if (!parsedContent || typeof parsedContent !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Handle new format with tool_execution
|
||||
if ('tool_execution' in parsedContent && typeof parsedContent.tool_execution === 'object') {
|
||||
const toolExecution = parsedContent.tool_execution;
|
||||
const args = toolExecution.arguments || {};
|
||||
|
||||
let parsedOutput = toolExecution.result?.output;
|
||||
if (typeof parsedOutput === 'string') {
|
||||
try {
|
||||
parsedOutput = JSON.parse(parsedOutput);
|
||||
} catch (e) {
|
||||
// Keep as string
|
||||
}
|
||||
}
|
||||
|
||||
const extractedData: any = {
|
||||
messageId: args.message_id,
|
||||
success: toolExecution.result?.success,
|
||||
timestamp: toolExecution.execution_details?.timestamp
|
||||
};
|
||||
|
||||
// Extract message and status from output
|
||||
if (parsedOutput && typeof parsedOutput === 'object') {
|
||||
extractedData.status = parsedOutput.status;
|
||||
extractedData.message = parsedOutput.message || parsedOutput.content;
|
||||
} else if (typeof parsedOutput === 'string') {
|
||||
extractedData.message = parsedOutput;
|
||||
}
|
||||
|
||||
return extractedData;
|
||||
}
|
||||
|
||||
// Handle content wrapper
|
||||
if ('role' in parsedContent && 'content' in parsedContent) {
|
||||
return extractFromNewFormat(parsedContent.content);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const extractFromLegacyFormat = (content: any): {
|
||||
messageId?: string;
|
||||
message?: string;
|
||||
status?: string;
|
||||
} => {
|
||||
const toolData = extractToolData(content);
|
||||
|
||||
if (toolData.arguments || toolData.toolResult) {
|
||||
const result: any = {
|
||||
messageId: toolData.arguments?.message_id
|
||||
};
|
||||
|
||||
const output = toolData.toolResult?.toolOutput;
|
||||
if (output) {
|
||||
if (typeof output === 'object' && output !== null) {
|
||||
result.status = (output as any).status;
|
||||
result.message = (output as any).message || (output as any).content;
|
||||
} else if (typeof output === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(output);
|
||||
result.status = parsed.status;
|
||||
result.message = parsed.message || parsed.content;
|
||||
} catch {
|
||||
result.message = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export function extractExpandMessageData(
|
||||
assistantContent: any,
|
||||
toolContent: any,
|
||||
isSuccess: boolean,
|
||||
toolTimestamp?: string,
|
||||
assistantTimestamp?: string
|
||||
): ExpandMessageData {
|
||||
let messageId: string | undefined;
|
||||
let message: string | undefined;
|
||||
let status: string | undefined;
|
||||
let actualIsSuccess = isSuccess;
|
||||
let actualToolTimestamp = toolTimestamp;
|
||||
const actualAssistantTimestamp = assistantTimestamp;
|
||||
|
||||
// Try new format first
|
||||
const assistantNewFormat = extractFromNewFormat(assistantContent);
|
||||
const toolNewFormat = extractFromNewFormat(toolContent);
|
||||
|
||||
// Extract from assistant content (parameters)
|
||||
if (assistantNewFormat.messageId) {
|
||||
messageId = assistantNewFormat.messageId;
|
||||
}
|
||||
|
||||
// Extract from tool result (output)
|
||||
if (toolNewFormat.message || toolNewFormat.status) {
|
||||
message = toolNewFormat.message;
|
||||
status = toolNewFormat.status;
|
||||
if (toolNewFormat.success !== undefined) {
|
||||
actualIsSuccess = toolNewFormat.success;
|
||||
}
|
||||
if (toolNewFormat.timestamp) {
|
||||
actualToolTimestamp = toolNewFormat.timestamp;
|
||||
}
|
||||
} else {
|
||||
// Try legacy format
|
||||
const assistantLegacy = extractFromLegacyFormat(assistantContent);
|
||||
const toolLegacy = extractFromLegacyFormat(toolContent);
|
||||
|
||||
messageId = messageId || assistantLegacy.messageId || toolLegacy.messageId;
|
||||
message = toolLegacy.message;
|
||||
status = toolLegacy.status;
|
||||
}
|
||||
|
||||
return {
|
||||
messageId,
|
||||
message,
|
||||
status,
|
||||
actualIsSuccess,
|
||||
actualToolTimestamp,
|
||||
actualAssistantTimestamp
|
||||
};
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ import ListAgentWorkflowsToolView from '../list-agent-workflows/list-agent-workf
|
|||
import { createPresentationViewerToolContent, parsePresentationSlidePath } from '../utils/presentation-utils';
|
||||
import { extractToolData } from '../utils';
|
||||
import { KbToolView } from '../KbToolView';
|
||||
import { ExpandMessageToolView } from '../expand-message-tool/ExpandMessageToolView';
|
||||
|
||||
|
||||
export type ToolViewComponent = React.ComponentType<ToolViewProps>;
|
||||
|
@ -122,6 +123,8 @@ const defaultRegistry: ToolViewRegistryType = {
|
|||
'ask': AskToolView,
|
||||
'complete': CompleteToolView,
|
||||
'wait': WaitToolView,
|
||||
'expand_message': ExpandMessageToolView,
|
||||
'expand-message': ExpandMessageToolView,
|
||||
|
||||
'deploy': DeployToolView,
|
||||
|
||||
|
|
Loading…
Reference in New Issue